23 KiB
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
- Design Philosophy
- Visual Identity
- Component Architecture
- UX Patterns
- Svelte 5 Implementation
- PWA Configuration
- Animation & Motion
- Data & State Management
- Responsive Design
- Accessibility
- Performance Budget
- Project Structure
- AI Agent Workflow
- 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
- Dark canvas, warm accents — The background is always dark (#070A0D). Light is used sparingly as accent, never as background.
- Typographic hierarchy — Three voices: Cinzel (display/serif — authority), DM Sans (body — readability), JetBrains Mono (data/meta — precision).
- Geometric intentionality — The golden ratio, polygons, and clip-paths aren't decorative gimmicks. They echo Islamic geometric tradition in a modern way.
- Motion with purpose — Transitions are brief (150-300ms). They communicate state change, not entertain. No idle animations.
- 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-smvariable) - 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 markicon-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
transformandopacityonly — never animatingwidth,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:
swapto 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
- Read this guide first. Every decision in the guide exists for consistency.
- Study one existing component (e.g.,
PrayerTimes.svelte) to understand patterns before writing new code. - Use the existing CSS variables — never introduce new colors. If you need a new accent, use
--goldor--emerald. - One component per feature. Features go in
src/lib/. No subdirectories unless you have 5+ related components. - Tab-based navigation. Every new feature is a new tab in
App.svelte. No routes, no modals, no nested views. - Test offline. Run the built app with network throttling. Core feature must work without internet.
- Check bundle size. Run
npm run buildand 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:
- Docker Swarm (primary) — nginx:alpine behind Traefik on Contabo VPS
- 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_keynaming
FalahOS — Sovereign Digital Economy. Every app built on these principles carries the same promise: private, premium, purposeful.