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 activeThemeVersion), 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
| Primitive | API summary | Web | Native | Notes |
|---|---|---|---|---|
Box | { padding?: Space; margin?: Space; bg?: ColorToken; border?: BorderToken; radius?: RadiusToken; ... } | <div> | <View> | Accepts logical paddings (paddingStart, paddingEnd) — RTL-aware |
Stack | direction-agnostic stack | flex column or row per direction prop | same | gap uses spacing tokens |
HStack | horizontal stack | flex row | same | RTL flips order via direction |
VStack | vertical stack | flex column | same | |
Center | center children | flex center | same |
4.2 Typography
| Primitive | API | Notes |
|---|---|---|
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 / Kbd | inline mono | uses fontFamilyMono |
4.3 Form
| Primitive | API summary | Notes |
|---|---|---|
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 / Switch | standard | All bidi-friendly; clickable label area meets 44 px |
DatePicker / BookingDateRangePicker | accepts calendar system (gregorian|islamic|persian); always emits ISO-8601 Gregorian | Uses calendar engine in §5 |
NumberInput | { value, onChange, min?, max?, step? } | inputMode="numeric"; locale-aware separators on display; raw integer on emit |
4.4 Overlay
| Primitive | Notes |
|---|---|
Modal | Focus trap, ESC to dismiss, overlay scrim from --color-overlay, return focus on close |
Drawer | Side-anchored sheet (start/end-aware) |
Sheet | Bottom sheet (mobile-friendly) |
Popover | Floating UI (uses @floating-ui/react web; custom on native) |
Tooltip | Delay 300 ms; touch devices use long-press; never the only affordance for an action |
4.5 Feedback
| Primitive | Notes |
|---|---|
Badge / Tag | Status semantics; tone matches success/warning/error/info/neutral |
Avatar | Initials fallback; alt text required |
Card | Container with surface background + radius + subtle shadow |
Divider | Horizontal or vertical; uses --color-divider |
Spinner | Honors reduced-motion (renders a static dot) |
Skeleton | Shimmer 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
| Primitive | Web only | Native only | Both |
|---|---|---|---|
Box, Stack, Text, Heading, Button, IconButton, Input, Checkbox, Radio, Switch, Modal, Drawer, Tooltip, Badge, Tag, Avatar, Card, Divider, Spinner, Skeleton, Image, Sheet, Popover | — | — | ✓ |
Select | rich popover variant | platform picker variant | base API |
DatePicker | inline + popover | inline + bottom-sheet | base API |
NumberInput | spinner buttons | stepper buttons | base API |
5. Components
Higher-level, domain-aware components composed from primitives.
| Component | Purpose | Used by |
|---|---|---|
RoomTypeCard | Room type with photo, beds, max occupancy, amenity chips, "from {price}" | tenant booking, mobile |
RatePlanCard | Rate plan summary with refundability, breakdown CTA, "Choose this rate" | tenant booking, mobile |
BookingDateRangePicker | Two-month range picker, blocked dates from availability, RTL-flipped | meta + tenant + mobile |
OccupancyPicker | Adults / children / rooms ± buttons with min/max enforcement | meta + tenant + mobile |
AmenityChips | Chip row with overflow "+N more" disclosure | listing card, detail page |
PropertyMap | Leaflet (web) / react-native-maps (mobile) wrapper with brand-colored pins, clustering | meta search, detail |
GalleryGrid | Responsive photo grid with lightbox; mosaic / carousel / grid layouts | content blocks, detail |
TestimonialCard | Quote, author, optional rating; locale-aware | content blocks |
PricingDisplay | Multi-currency, FX-snapshot aware; renders display.formatted; flags stale snapshots | tenant booking, confirmation |
KeyCredentialBadge | Mobile-key / PIN / RFID / QR variants; pending vs delivered states | confirmation, mobile manage |
HousekeepingTaskCard | Task w/ priority, room status pill, AI-suggested order indicator | desktop |
ReservationStatusPill | Status (held/confirmed/checked_in/checked_out/no_show/cancelled) with a11y label | desktop, confirmation |
OperatorActivityFeed | Time-ordered events (check-ins, modifications, payments, key issuances) | desktop |
AISuggestionCard | AI proposal with provenance footer ({ model, version, promptId, traceId, local }); accept / dismiss / "Why this?" | desktop, tenant authoring |
AlertBanner | Page-level banners (info/warning/error/success) with optional action | all |
OfflineBanner | Sticky banner — "You're offline. Browsing still works." | meta web, mobile, desktop |
SyncStatusBadge | Sync 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
| Hook | Returns | Notes |
|---|---|---|
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) => string | Latin-numeral, locale-aware |
useFormatDate() | (iso: string, opts?) => string | Calendar-aware (Gregorian/Hijri/Solar Hijri) |
useFormatNumber() | (n: number, opts?) => string | Locale-aware |
useDeviceType() | 'mobile' | 'tablet' | 'desktop' | Web: viewport-derived; native: Dimensions + tablet detector |
useReducedMotion() | boolean | OS preference; throttled via subscription |
useNetworkStatus() | { online: boolean; saveData: boolean; effectiveType?: '2g'|'3g'|'4g'|'5g' } | Web: NIC API; native: @react-native-community/netinfo |
useFeatureFlag(key) | boolean | Reads from BFF bootstrap's featureFlags map; never call domain services directly |
useFocusVisible() | { ref, isFocusVisible } | Wrapper for :focus-visible semantics on both platforms |
useDirectionalIcon(name) | IconKey | Returns 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 group | UI font (priority) | Fallback chain | Numeral system | Notes |
|---|---|---|---|---|
Latin (en-*, fr-*, de-*, es-*, it-*) | Inter (variable) | system-ui, -apple-system, sans-serif | Latin | Default body + heading |
Pashto (ps-*), Dari (fa-AF) | Vazirmatn | Noto Naskh Arabic, system-ui, sans-serif | Latin (UI) | Vazirmatn covers Pashto + Dari + Persian |
Persian (fa-IR) | Vazirmatn | Noto Naskh Arabic, system-ui, sans-serif | Latin (toggle to Persian via locale pack) | |
Arabic (ar-*) | Noto Naskh Arabic | Vazirmatn, system-ui, sans-serif | Latin | |
Urdu (ur-PK) | Noto Nastaliq Urdu | Noto Naskh Arabic, system-ui, sans-serif | Latin | Phase 1 |
| Mono (all) | JetBrains Mono | ui-monospace, monospace | Latin | Code, 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 (
woff2withunicode-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, noblue-700). Token names express role, not value. - Modes:
light,dark,high-contrast— three published bundles per tenant. Default tenants shiplightonly; opting into dark or high-contrast enables additional bundles. - Auto-generated tints/shades: at publish time,
theme-config-servicederivesprimaryHover/primaryActivefromprimaryif 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-serviceand at every PR in the design system CI (against the Melmastoon defaults). Failures block the build. - Status palette:
success/warning/error/infoare tenant-overridable but constrained to a hue range to remain semantically obvious (e.g.,errormust 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 pickunit ∈ {4, 6, 8}. - Density modes:
cozy/comfortable(default) /compact. Density is consumer-chosen on the desktop backoffice (operator preference); guest surfaces staycomfortable.
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-endetc.; ESLint rejects rawpaddingLeft/marginRightin 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(); ontrue, 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
| Helper | Purpose |
|---|---|
<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
| Helper | Purpose |
|---|---|
<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 onserious/criticalviolations. - Native stories: parallel stories live in
apps/mobile/storybookfor 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:
| Rule | Purpose | Enforcement |
|---|---|---|
no-raw-color | Forbid hex / rgb / hsl in apps/* and packages/feature-* | Error |
no-raw-spacing | Forbid raw px / rem in style props or className utilities (p-[12px]) | Error |
no-raw-font | Forbid font-family overrides outside @ghasi/ui-melmastoon/tokens | Error |
no-physical-properties | Forbid padding-left/padding-right, margin-left/margin-right, left/right (use logical) | Error |
no-non-translated-text | Detect string literals in JSX (with allow-list for testids, hrefs, etc.) | Warning → Error in CI |
no-direct-domain-call | Imports from @ghasi/api-clients/<bff> only — direct service URLs forbidden | Error |
min-touch-target | Button/IconButton/Pressable must declare a size that resolves ≥ 44×44 mobile / 32×32 web | Warning |
prefer-primitive | Discourage ad-hoc <div> for layout when Box/Stack would do | Warning |
icon-only-needs-label | IconButton must have non-empty label | Error |
i18n-key-exists | Translation keys passed to t() must exist in the catalog at build time | Error in CI |
axe-a11y | Storybook + Playwright @axe-core checks run in CI | Block on serious/critical |
zod-schema-required | Components 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:
- Loads tokens (defaults merged with tenant overrides).
- Emits CSS variables on web (memoized stable object).
- Provides the resolved
DesignTokensvia context foruseTheme()consumers. - Wraps
DirectionProviderso RTL detection works. - 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, packagepackages/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 publishto 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.mdwith codemods (@ghasi/ui-melmastoon-codemods) for mechanical rewrites.
18. Anti-patterns
| Anti-pattern | Why it's banned | Correct approach |
|---|---|---|
Raw hex colors in app code (color: '#0F4C81', bg-[#0F4C81]) | Defeats tenant theming + contrast invariants; breaks dark/high-contrast modes | Use semantic tokens via Tailwind utilities (web) or useTheme() (mobile) |
Bypassing primitives for ad-hoc styled <div>s | Inconsistent spacing/typography/focus; loses RTL behavior | Compose 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 layouts | Use logical properties; <Icon name="chevron-right" /> mirrors automatically; text-align: start/end |
Non-translatable text in components (<p>Book now</p>) | Breaks i18n and audit | Use useTranslations / pass an i18n key prop |
Icon-only buttons without label | Screen reader users have no clue what the button does | <IconButton icon="key" label={t('button.issue_key')} /> — label is required |
Spinner indefinitely without a fallback | Reduced-motion users get nothing; long waits give no escape hatch | Pair 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 tooltips | Always pair with helper text or aria-describedby |
<input> / <button> not driven by primitives | Loses focus rings, hit areas, density modes, locale-aware numeric handling | Use Input / Button |
| Embedding raw SVG in components | Bypasses the icon manifest (no RTL, no naming, no a11y guarantees) | Add to @ghasi/icons, use <Icon name="..."> |
| Custom date picker in app code | Calendar fragmentation (Gregorian vs Hijri vs Solar Hijri) breaks user expectations | Use BookingDateRangePicker / DatePicker |
App-level dangerouslySetInnerHTML outside MarkdownBlock | XSS surface | Render through MarkdownBlock (sanitized) |
Mutating melmastoonDefaultTokens at runtime | Breaks identity stability + memoization assumptions | Override via tenant theme; never mutate the default object |
| Skipping Storybook stories for new components | Reviewers can't validate states, locales, RTL; visual regression has no baseline | Stories are required for any new public export — CI enforces |
| Per-app forks of a primitive | Diverges API; breaks design system promise | Add a variant to the primitive in @ghasi/ui-melmastoon instead |
19. References
- Token domain model:
services/theme-config-service/DOMAIN_MODEL.md - Frontend theming consumer guide:
02-theming-and-tenant-config.md - Web/mobile spec:
01-web-and-mobile-specification.md - Desktop spec (consumes the design system in the renderer):
desktop/06-desktop-app-specification.md - Engineering standards (TS, monorepo, naming):
docs/standards/01-engineering-standards.md - Naming + error code registry:
docs/standards/NAMING.md,docs/standards/ERROR_CODES.md - Testing strategy:
docs/testing/01-testing-strategy-qa.md