Files
nur-muslim-companion/FALAHOS-DESIGN-GUIDE.md

23 KiB
Raw Permalink Blame History

FalahOS Design & App Building Guide

Purpose: This document defines the FalahOS design system, UI/UX principles, and Svelte 5 implementation patterns. AI agents building or extending FalahOS apps MUST read and follow this guide. Every design decision in this guide exists for a reason — deviations should be the exception, not the habit.


Table of Contents

  1. Design Philosophy
  2. Visual Identity
  3. Component Architecture
  4. UX Patterns
  5. Svelte 5 Implementation
  6. PWA Configuration
  7. Animation & Motion
  8. Data & State Management
  9. Responsive Design
  10. Accessibility
  11. Performance Budget
  12. Project Structure
  13. AI Agent Workflow
  14. Checklist: New Feature

1. Design Philosophy

Core Principles

Principle Meaning
Sovereign by default No third-party dependencies for core UX. No tracking, no analytics, no external fonts where avoidable. The app works without a server.
Privacy by design Zero data collection. All processing is on-device. No accounts. No cookies.
Premium minimalism High visual polish with minimal UI chrome. Every pixel earns its place. Dark theme is not a fallback — it's the canvas.
Islamic essence, modern execution The spiritual context informs every design choice without resorting to clichés. Gold and green are not decorations — they carry meaning.
Offline-first The app must be useful without internet. Core features (Qibla, Tasbih, Quran cache, Hijri) work fully offline after initial load.

Design Tenets

  1. Dark canvas, warm accents — The background is always dark (#070A0D). Light is used sparingly as accent, never as background.
  2. Typographic hierarchy — Three voices: Cinzel (display/serif — authority), DM Sans (body — readability), JetBrains Mono (data/meta — precision).
  3. Geometric intentionality — The golden ratio, polygons, and clip-paths aren't decorative gimmicks. They echo Islamic geometric tradition in a modern way.
  4. Motion with purpose — Transitions are brief (150-300ms). They communicate state change, not entertain. No idle animations.
  5. Content over chrome — Navigation is compact (tabs, not hamburger menus). Cards have thin borders, not heavy shadows. Information density is high for the feature, low for chrome.

2. Visual Identity

2.1 Color System

:root {
  /* Backgrounds — deep navy-black */
  --bg:           #070A0D;   /* Page background */
  --surface:      #0D1117;   /* Card/section surface */
  --card:         #111820;   /* Elevated cards, overlays */
  --border:       #1E2A36;   /* Subtle borders */

  /* Text */
  --text:         #E8E6E1;   /* Primary body text */
  --text-dim:     #8B8B8B;   /* Secondary/meta text */
  --text-muted:   #5A5A5A;   /* Footer/legal text */

  /* Accents */
  --gold:         #C9A84C;   /* Primary accent — faith, value, premium */
  --gold-light:   #D4B95A;   /* Gold hover/gradient variant */
  --gold-dim:     rgba(201, 168, 76, 0.15);  /* Gold background tint */

  --emerald:      #2ECC71;   /* Secondary accent — growth, success, nature */
  --green-dim:    rgba(46, 204, 113, 0.12);  /* Emerald background tint */

  /* Functional */
  --error:        #E74C3C;   /* Errors, destructive actions */
  --warning:      #F39C12;   /* Warnings */
  --info:         #3498DB;   /* Information */
}

Usage rules:

  • Gold is for UI that carries spiritual/faith significance: active tabs, prayer times, headings, CTAs
  • Emerald is for positive states, success indicators, confirmed data
  • Never use white (#FFFFFF) — use --text (#E8E6E1) as the lightest color
  • Never use pure black (#000000) — use --bg (#070A0D)
  • Borders are always --border, never a lighter shade
  • Text on gold backgrounds uses --bg (inverted for contrast)

2.2 Typography

Voice assignments:

Font Role Weight Size Scale
Cinzel (Google Fonts, serif) Display headings, brand name, primary CTAs 700-900 1.0-2.0rem
DM Sans (Google Fonts, sans-serif) Body text, labels, paragraph content 300-700 0.75-1.1rem
JetBrains Mono (Google Fonts, monospace) Tab labels, meta text, footer, data values 400-500 0.55-0.75rem

Implementation:

/* app.html */
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@700;900&family=DM+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />

/* CSS variables */
--font-display: 'Cinzel', serif;
--font-body: 'DM Sans', sans-serif;
--font-mono: 'JetBrains Mono', monospace;

Type scale:

1.8rem — Brand name (Cinzel 900)
1.3rem — Tab icons (emoji/unicode)
1.0rem — Card headings (Cinzel 700)
0.85rem — Body text (DM Sans 400)
0.72rem — Button labels, data (JetBrains Mono 500)
0.62rem — Tab text, footer (JetBrains Mono 400, 0.25em letter-spacing)

2.3 Spacing System

Use a 4px baseline grid. Common values:

--space-xs: 4px;
--space-sm: 8px;
--space-md: 12px;
--space-lg: 16px;
--space-xl: 20px;
--space-2xl: 24px;
  • Card padding: 20px (--space-xl)
  • Section gaps: 12px (--space-md)
  • Tab padding: 8px 10px (--space-sm variable)
  • Content max-width: 480px (mobile-first)

2.4 Surfaces & Depth

/* Card */
.card {
  background: var(--surface);      /* or var(--card) for elevated cards */
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 20px;
  box-shadow:
    0 4px 20px rgba(0, 0, 0, 0.3),
    inset 0 1px 0 rgba(201, 168, 76, 0.05);
}

/* Header / Nav bar — glassmorphism */
header, .tab-nav, .footer {
  background: rgba(12, 17, 23, 0.8);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border-bottom: 1px solid var(--border);
  border-radius: 0 0 24px 24px;
}

2.5 Background Effects

Noise texture — applied via SVG filter on the <body> pseudoelement:

body::before {
  content: '';
  position: fixed; inset: 0; z-index: 0;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' ... %3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E");
  pointer-events: none;
  opacity: 0.4;
}

Geometric light — subtle radial gradients for depth:

body::after {
  content: '';
  position: fixed; inset: 0; z-index: 0;
  background:
    radial-gradient(ellipse 80% 60% at 50% -10%, rgba(201,168,76,0.06) 0%, transparent 60%),
    radial-gradient(ellipse 40% 40% at 90% 80%, rgba(46,204,113,0.04) 0%, transparent 50%),
    radial-gradient(ellipse 60% 40% at -10% 50%, rgba(201,168,76,0.03) 0%, transparent 50%);
  pointer-events: none;
}

3. Component Architecture

3.1 Component Categories

Category Convention Examples
Layout src/lib/layout/ Header.svelte, TabNav.svelte, Footer.svelte
Feature src/lib/*.svelte (flat) PrayerTimes.svelte, Qibla.svelte, Quran.svelte
UI Primitives Inline in feature or global styles Card, button (via :global())
State/Data Local to component via $state runes No global stores unless truly shared

3.2 Component Structure (Svelte 5)

<script>
  // 1. $state declarations
  let data = $state(null);
  let loading = $state(true);

  // 2. Lifecycle (onMount, etc.)
  import { onMount } from 'svelte';
  onMount(() => { /* init */ });

  // 3. Functions
  async function loadData() { /* ... */ }

  // 4. Derived values
  let displayValue = $derived(data ?? '—');
</script>

<!-- Template: semantic HTML, minimal wrappers -->
<div class="card">
  <h2>🕌 Title</h2>
  <!-- ... -->
</div>

<style>
  /* Scoped styles. Use :global() sparingly — only for app-wide primitives */
  .card { /* ... */ }
</style>

3.3 The Card Pattern

The .card is the primary content container. Every feature tab is wrapped in one or more cards:

<div class="card">
  <h2>📖 Feature Title</h2>
  <div class="card-body">
    <!-- content -->
  </div>
</div>

Cards stack vertically with 12px (--space-md) margin-bottom.

3.4 Button System

<!-- Primary CTA — for faith-significant actions -->
<button class="primary">Save Settings</button>

<!-- Secondary — for cancellable/optional actions -->
<button class="secondary">Cancel</button>

<!-- Tab button — navigation tab -->
<button class="tab" class:active={isActive}>
  <span class="tab-icon">🕌</span>
  <span class="tab-label">PRAYER</span>
</button>

Primary button style:

button.primary {
  font-family: var(--font-display);   /* Cinzel */
  font-size: 0.82rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 10px 20px;
  background: linear-gradient(135deg, var(--gold), var(--gold-light));
  color: var(--bg);
  border-radius: 10px;
  clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 10px 100%, 0 calc(100% - 10px));
  /* Dipped corner is a signature FalahOS detail */
}

4. UX Patterns

4.1 Tab Navigation

The app uses a flat tab bar — no nested navigation, no hamburger menus, no back buttons. All features are one tap away.

<nav class="tab-nav">
  {#each tabs as tab, i}
    <button class="tab" class:active={activeTab === i}
            onclick={() => activeTab = i}>
      <span class="tab-icon">{icons[i]}</span>
      <span class="tab-label">{tab}</span>
    </button>
  {/each}
</nav>

<main>
  {#if activeTab === 0}
    <FeatureA />
  {:else if activeTab === 1}
    <FeatureB />
  {/each}
</main>

Keyboard navigation: ArrowLeft/ArrowRight cycles tabs:

function handleKeydown(e) {
  if (e.key === 'ArrowRight') activeTab = (activeTab + 1) % tabs.length;
  if (e.key === 'ArrowLeft') activeTab = (activeTab - 1 + tabs.length) % tabs.length;
}

4.2 Loading States

Every data-dependent component must handle four states:

{#if loading}
  <div class="loading">Loading...</div>
{:else if error}
  <div class="error">{error}</div>
{:else if data}
  <!-- render content -->
{:else}
  <div class="empty">No data available</div>
{/if}
  • Loading: Show a centered, subtle loading indicator (no spinners — use text or skeleton)
  • Error: Show error message in gold/red with a retry action
  • Empty: Show a helpful message, not a blank screen
  • Success: Render the data

4.3 Notification Permissions

Prayer notification toggle uses a graceful permission flow:

async function requestNotificationPermission() {
  if (!('Notification' in window)) return false;
  const perm = await Notification.requestPermission();
  if (perm === 'granted') {
    // Schedule prayer notifications
  } else if (perm === 'denied') {
    // Show a non-blocking hint about browser settings
  }
}

Notifications are opt-in, never assumed. The checkbox is visible but unchecked by default.

4.4 Data Persistence

Use localStorage for user preferences only. Never use it for app data.

function saveMethod() {
  localStorage.setItem('prayer_method', String(method));
}

function loadSettings() {
  const saved = localStorage.getItem('prayer_method');
  if (saved) method = parseInt(saved, 10);
}

5. Svelte 5 Implementation

5.1 Runes — The Only Pattern

Use $state for all reactive declarations. Never use let alone for reactive variables.

<script>
  // ✅ CORRECT
  let count = $state(0);
  let data = $state(null);
  let items = $state([]);

  // ❌ WRONG — not reactive
  let count = 0;
</script>

5.2 $derived

Use $derived for computed values that depend on state:

<script>
  let count = $state(0);
  let double = $derived(count * 2);
</script>

5.3 $effect

Use $effect sparingly — only for side effects that interact with the outside world (localStorage, DOM APIs, timers):

<script>
  let method = $state(3);

  $effect(() => {
    localStorage.setItem('prayer_method', String(method));
  });
</script>

Do NOT use $effect for:

  • Derived values (use $derived)
  • Event handlers (use onclick, oninput, etc.)
  • Fetch calls triggered by state changes (use async functions called from events)

5.4 No Stores Unless Necessary

For single-page tab apps, component-local state with $state is sufficent. Only introduce Svelte stores (or $state in a shared module) when two or more unrelated components need the same reactive value.

If needed, use a .js module with module-level $state:

// src/lib/stores.js
let activeTab = $state(0);
export function getActiveTab() { return activeTab; }
export function setActiveTab(n) { activeTab = n; }

5.5 Event Handling

Use modern Svelte 5 event syntax (not on:click):

<!-- Svelte 5 ✅ -->
<button onclick={() => handleClick()}>Click</button>
<select onchange={(e) => method = Number(e.target.value)}>

5.6 Component Composition

Pass data down via props, never via shared mutable state:

<ChildComponent data={prayers} onSelect={handleSelect} />

Props are not declared with export let in Svelte 5. Use $props():

<script>
  let { data, onSelect } = $props();
</script>

6. PWA Configuration

6.1 vite-plugin-pwa Setup

// vite.config.js
import { VitePWA } from 'vite-plugin-pwa';

VitePWA({
  registerType: 'autoUpdate',
  manifest: {
    name: 'Nur Falah — Muslim Companion',
    short_name: 'Nur Falah',
    description: 'Your daily Islamic companion',
    theme_color: '#070A0D',
    background_color: '#070A0D',
    display: 'standalone',
    orientation: 'portrait-primary',
    icons: [
      { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
      { src: '/icon-512.png', sizes: '512x512', type: 'image/png' }
    ]
  },
  workbox: {
    globPatterns: ['**/*.{js,css,html,svg,png,woff2,ico}'],
    runtimeCaching: [
      {
        urlPattern: /^https:\/\/api\.alquran\.cloud\/.*/i,
        handler: 'StaleWhileRevalidate',
        options: {
          cacheName: 'quran-api',
          expiration: { maxEntries: 50, maxAgeSeconds: 86400 * 7 }
        }
      }
    ],
    cleanupOutdatedCaches: true
  }
})

6.2 app.html Meta

<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#070A0D" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="preconnect" href="https://api.alquran.cloud" crossorigin />

6.3 Service Worker

The registerType: 'autoUpdate' ensures users always get the latest version. The service worker handles:

  • Pre-caching all static assets
  • Runtime caching for the Quran API (7-day cache, 50 entries max)
  • Cleanup of outdated caches on new versions

6.4 App Icons

  • icon-192.png — 192×192px, emerald/gold brand mark
  • icon-512.png — 512×512px, same design
  • Favicon: SVG for clean rendering at any size

7. Animation & Motion

7.1 Principles

  • Brief: All transitions 150-300ms
  • Purposeful: Motion communicates state change only
  • Subtle: No bounces, no parallax, no idle animations
  • Perf: Use CSS transform and opacity only — never animating width, height, top, left

7.2 Transition Presets

/* Tab hover */
.tab {
  transition: all 0.2s;
}
.tab:hover { color: var(--gold); }
.tab.active {
  color: var(--gold);
  background: var(--gold-dim);
}

/* Primary button hover */
button.primary {
  transition: transform 0.2s, box-shadow 0.2s;
}
button.primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 30px rgba(201, 168, 76, 0.3);
}
button.primary:active { transform: translateY(0); }

/* Secondary button */
button.secondary {
  transition: all 0.2s;
}
button.secondary:hover {
  border-color: var(--gold);
  color: var(--gold);
}

7.3 Page Transitions

For tab switching, use Svelte 5 transitions:

{#key activeTab}
  <main transition:fade={{ duration: 200 }}>
    <!-- tab content -->
  </main>
{/key}

{#key} forces the element to re-render on tab change, triggering the transition.


8. Data & State Management

8.1 Local Storage Keys

Key Type Purpose
prayer_method number Selected calculation method ID
prayer_lat number Cached latitude
prayer_lng number Cached longitude
prayer_notifications boolean Notification preference

All keys use snake_case with a feature prefix.

8.2 External API Calls

Chain API calls carefully. Nur Falah uses the AlQuran.cloud API:

async function fetchSurah(surahNumber) {
  const res = await fetch(`https://api.alquran.cloud/v1/surah/${surahNumber}`);
  if (!res.ok) throw new Error('Failed to fetch');
  return res.json();
}
  • Always handle errors gracefully (show a user-friendly message)
  • Cache API responses in the service worker (runtime caching)
  • Never expose API keys in client code (this API is public)

9. Responsive Design

9.1 Mobile-First

.app {
  max-width: 480px;
  margin: 0 auto;
  min-height: 100dvh;
}

The app is designed for mobile portrait. On desktop, it centers in a phone-like container. Do NOT create a separate desktop layout — the mobile experience IS the experience.

9.2 Safe Areas

/* Use viewport-fit=cover in meta tag */
padding-bottom: 80px;        /* Tab bar clearance */
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom, 80px);

9.3 Touch Targets

  • Tab buttons: minimum 56px wide
  • Interactive elements: minimum 44×44px touch target
  • Dropdowns: use native <select> for reliable touch behavior

10. Accessibility

10.1 ARIA Labels

<button class="tab" aria-label={tab} onclick={() => activeTab = i}>

Every interactive element needs an aria-label if the visual content is icon-only.

10.2 Focus Management

Tab navigation uses keyboard arrows (ArrowLeft/ArrowRight). Provide a svelte:window handler:

<svelte:window onkeydown={handleKeydown} />

10.3 Color Contrast

All text/background combinations exceed WCAG AA contrast:

  • Gold (#C9A84C) on dark bg (#070A0D): ratio ≈ 5.8:1
  • Text (#E8E6E1) on surface (#0D1117): ratio ≈ 14.0:1
  • Dim text (#8B8B8B) on surface (#0D1117): ratio ≈ 5.5:1

11. Performance Budget

Metric Target
Total JS bundle (gzipped) < 45KB
Total CSS (gzipped) < 8KB
First paint < 1s on 4G
Lighthouse PWA score 100
Offline capability Qibla, Tasbih, Hijri, Quran (cached)
Service worker size < 2KB

11.1 Bundle Optimization

  • No heavy UI libraries — CSS and Svelte are sufficient
  • No icon libraries — use emoji for icons (zero bundle cost, universal)
  • Font display: swap to prevent FOIT
  • Preconnect to API origins

12. Project Structure

project-root/
├── public/
│   ├── privacy.html          # Standalone privacy page (static)
│   ├── icon-192.png
│   ├── icon-512.png
│   ├── favicon.svg
│   └── sw.js                 # Offline fallback (auto-generated by vite-plugin-pwa)
├── src/
│   ├── lib/
│   │   ├── __tests__/        # Vitest unit tests
│   │   ├── PrayerTimes.svelte
│   │   ├── Qibla.svelte
│   │   ├── Quran.svelte
│   │   ├── Hijri.svelte
│   │   ├── Tasbih.svelte
│   │   └── Names99.svelte
│   ├── App.svelte            # Main app shell (tabs, nav, footer, global styles)
│   ├── app.html              # HTML shell (fonts, meta, preconnect)
│   └── main.js               # Entry point
├── scripts/                  # Build/deploy/utility scripts
├── appstore-assets/
│   └── screenshots/          # App Store screenshot mockups
├── package.json
├── vite.config.js
└── svelte.config.js

12.1 Naming Conventions

Artifact Convention Example
Components PascalCase.svelte PrayerTimes.svelte
Scripts kebab-case generate-docx.mjs
Assets kebab-case icon-192.png
CSS classes kebab-case .tab-nav, .gold-dim
CSS variables kebab-case --font-display
JS functions camelCase fetchPrayerTimes()
localStorage keys snake_case prayer_method

13. AI Agent Workflow

13.1 When Building a New Feature

  1. Read this guide first. Every decision in the guide exists for consistency.
  2. Study one existing component (e.g., PrayerTimes.svelte) to understand patterns before writing new code.
  3. Use the existing CSS variables — never introduce new colors. If you need a new accent, use --gold or --emerald.
  4. One component per feature. Features go in src/lib/. No subdirectories unless you have 5+ related components.
  5. Tab-based navigation. Every new feature is a new tab in App.svelte. No routes, no modals, no nested views.
  6. Test offline. Run the built app with network throttling. Core feature must work without internet.
  7. Check bundle size. Run npm run build and check the dist output. Keep it under 45KB gzipped total.

13.2 When Modifying the Design System

  • Never change --bg, --text, --gold, --emerald — these are identity-defining
  • New CSS variables go in app.html's global <style>, documented in this guide
  • New fonts require an advocate — prefer extending the three existing voices

13.3 Deployment

Static files are built by Vite and deployed via:

  1. Docker Swarm (primary) — nginx:alpine behind Traefik on Contabo VPS
  2. Netlify (mirror) — direct deploy with netlify deploy --prod --dir=dist

13.4 Quick Reference: Add a New Tab

<!-- App.svelte changes -->
<script>
  import NewFeature from './lib/NewFeature.svelte';
  const tabs = [...tabs, 'New Feature'];
  const icons = [...icons, '📱'];
</script>

<!-- Template: add condition -->
{:else if activeTab === 6}
  <NewFeature />

14. Checklist: New Feature

Before submitting a feature as complete, verify:

  • Follows this design guide
  • Uses existing CSS variables and design tokens
  • Handles loading, error, empty, and success states
  • Works offline (if data was previously loaded)
  • Passes npm run build (no errors, no lint warnings)
  • Bundle size increase < 5KB gzipped
  • Tab navigation works with keyboard arrows
  • Touch targets ≥ 44×44px
  • No new colors introduced without justification
  • Privacy: no data collected or transmitted
  • localStorage keys use feature_key naming

FalahOS — Sovereign Digital Economy. Every app built on these principles carries the same promise: private, premium, purposeful.