Initial build: Horizon 1 Prevention Suite + Horizon 2 Unlock demo

Svelte 5 + Vite PWA, styled to match moslem03.falahos.my's design system.

Horizon 1: Faraid Calculator (shared calc core, 14 classical cases passing),
Asset Registry, Wassiyah Generator (1/3 meter + heir-exclusion block),
Hibah Tracker and Family Waqf Designator (shared marad al-mawt guardrail).

Horizon 2: Digital Beneficial Claims — local non-custodial demo of the claim
model and transfer restriction only.

Two governance gates in this project's own PRDs were overridden per explicit
product direction, and are flagged in-app and in README.md / deploy/DEPLOY.md
rather than silently shipped as production-ready:
- Family Waqf Designator ships ahead of scholarly sign-off (OPEN-01 in
  scholarly-review-log.md remains unresolved).
- Horizon 2 ships ahead of the PRD's stated Phase 0 gate (legal opinion +
  signed institutional partner) with no confirmation that gate is cleared.
This commit is contained in:
wmj
2026-08-13 16:55:58 +08:00
commit 6690696f7f
28 changed files with 7586 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
<script>
import FaraidCalculator from './lib/FaraidCalculator.svelte';
import AssetRegistry from './lib/AssetRegistry.svelte';
import WassiyahGenerator from './lib/WassiyahGenerator.svelte';
import HibahTracker from './lib/HibahTracker.svelte';
import FamilyWaqfDesignator from './lib/FamilyWaqfDesignator.svelte';
import DigitalClaims from './lib/horizon2/DigitalClaims.svelte';
import { exportAll, deleteAll } from './lib/storage.js';
const tabs = ['Faraid', 'Assets', 'Wassiyah', 'Hibah', 'Family Waqf', 'Claims (H2)', 'Settings'];
const icons = ['📊', '📁', '📜', '🎁', '⛲', '🔗', '⚙️'];
let activeTab = $state(0);
function handleKeydown(e) {
if (e.key === 'ArrowRight') activeTab = (activeTab + 1) % tabs.length;
if (e.key === 'ArrowLeft') activeTab = (activeTab - 1 + tabs.length) % tabs.length;
}
function doExport() {
const data = exportAll();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'nur-falah-full-export.json'; a.click();
URL.revokeObjectURL(url);
}
function doDelete() {
if (confirm('This permanently deletes all locally stored data. This cannot be undone. Continue?')) {
deleteAll();
location.reload();
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
<div class="app">
<header>
<div class="header-brand">
<div class="brand-line brand-line-first">Nur</div>
<div class="brand-line brand-line-second">Falah</div>
</div>
<span class="header-tagline">ESTATE &amp; WAQF SUITE</span>
<div class="header-divider"></div>
</header>
<nav>
{#each tabs as tab, i}
<button class="tab" class:active={activeTab === i} onclick={() => activeTab = i} aria-label={tab}>
<span class="tab-icon">{icons[i]}</span>
<span class="tab-label">{tab}</span>
</button>
{/each}
</nav>
<main>
{#if activeTab === 0}<FaraidCalculator />
{:else if activeTab === 1}<AssetRegistry />
{:else if activeTab === 2}<WassiyahGenerator />
{:else if activeTab === 3}<HibahTracker />
{:else if activeTab === 4}<FamilyWaqfDesignator />
{:else if activeTab === 5}<DigitalClaims />
{:else if activeTab === 6}
<div class="module">
<h2>Settings</h2>
<p class="sub">Your data is stored locally on this device. Nothing is sent to a third party.</p>
<button class="btn-secondary" onclick={doExport}>Export all data (JSON)</button>
<button class="btn-danger" onclick={doDelete}>Delete all data irreversible</button>
</div>
{/if}
</main>
</div>
<style>
:global(*) { box-sizing: border-box; margin: 0; padding: 0; }
:global(body) {
font-family: 'DM Sans', sans-serif;
background: #070A0D;
color: #E8E4DC;
min-height: 100dvh;
overflow-x: hidden;
position: relative;
}
:global(body::before) {
content: '';
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background:
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(201,168,76,0.08) 0%, transparent 70%),
radial-gradient(ellipse 60% 40% at 20% 80%, rgba(46,204,113,0.05) 0%, transparent 60%);
pointer-events: none; z-index: 0;
}
:global(#app) { position: relative; z-index: 2; }
.app { max-width: 480px; margin: 0 auto; min-height: 100dvh; display: flex; flex-direction: column; padding-bottom: 80px; }
header { text-align: center; padding: 28px 16px 16px; background: rgba(12,17,23,0.95); backdrop-filter: blur(12px); border-bottom: 1px solid rgba(201,168,76,0.15); position: sticky; top: 0; z-index: 10; }
.header-brand { display: flex; justify-content: center; gap: 8px; }
.brand-line { font-family: 'DM Serif Display', serif; font-size: 26px; }
.brand-line-first { color: #E8E4DC; }
.brand-line-second { color: #C9A84C; }
.header-tagline { font-size: 10px; letter-spacing: 2px; color: #8A8478; }
.header-divider { height: 2px; width: 40px; background: #C9A84C; margin: 10px auto 0; border-radius: 2px; }
nav { display: flex; overflow-x: auto; gap: 4px; padding: 10px 8px; background: rgba(12,17,23,0.7); border-bottom: 1px solid rgba(201,168,76,0.1); position: sticky; top: 92px; z-index: 9; }
.tab { flex-shrink: 0; display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 8px 12px; background: none; border: none; border-radius: 10px; cursor: pointer; color: #8A8478; }
.tab.active { background: rgba(201,168,76,0.12); color: #C9A84C; }
.tab-icon { font-size: 18px; }
.tab-label { font-size: 10px; white-space: nowrap; }
main { flex: 1; padding: 16px; }
:global(.btn-danger) { width: 100%; padding: 12px; border-radius: 8px; border: 1px solid rgba(239,68,68,0.4); background: rgba(239,68,68,0.1); color: #EF4444; font-weight: 600; cursor: pointer; margin-top: 12px; }
:global(.btn-secondary) { width: 100%; padding: 12px; border-radius: 8px; border: none; background: rgba(255,255,255,0.08); color: #E8E4DC; font-weight: 600; cursor: pointer; }
</style>