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

797 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](#1-design-philosophy)
2. [Visual Identity](#2-visual-identity)
3. [Component Architecture](#3-component-architecture)
4. [UX Patterns](#4-ux-patterns)
5. [Svelte 5 Implementation](#5-svelte-5-implementation)
6. [PWA Configuration](#6-pwa-configuration)
7. [Animation & Motion](#7-animation--motion)
8. [Data & State Management](#8-data--state-management)
9. [Responsive Design](#9-responsive-design)
10. [Accessibility](#10-accessibility)
11. [Performance Budget](#11-performance-budget)
12. [Project Structure](#12-project-structure)
13. [AI Agent Workflow](#13-ai-agent-workflow)
14. [Checklist: New Feature](#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
```css
: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:**
```css
/* 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:
```css
--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
```css
/* 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:
```css
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:
```css
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)
```svelte
<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:
```svelte
<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
```svelte
<!-- 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:**
```css
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.
```svelte
<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:
```js
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:
```svelte
{#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:
```js
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.
```js
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.
```svelte
<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:
```svelte
<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):
```svelte
<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`:
```js
// 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
<!-- 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:
```svelte
<ChildComponent data={prayers} onSelect={handleSelect} />
```
Props are not declared with `export let` in Svelte 5. Use `$props()`:
```svelte
<script>
let { data, onSelect } = $props();
</script>
```
---
## 6. PWA Configuration
### 6.1 vite-plugin-pwa Setup
```js
// 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
```html
<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
```css
/* 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:
```svelte
{#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:
```js
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
```css
.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
```css
/* 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
```svelte
<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
<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
```svelte
<!-- 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.