Add FalahOS design & app building guide for AI agents
This commit is contained in:
@@ -0,0 +1,796 @@
|
|||||||
|
# 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.
|
||||||
Reference in New Issue
Block a user