Add (i) info button to every tab, explained for the average user

New InfoPanel.svelte: shared (i) button next to each module's h2, toggling
a plain-language explainer with three sections — what this tab is, how to
use it, what to enter in each field. One component so tone stays
consistent across all 10 tabs instead of each screen inventing its own
help pattern.

Deliberately avoids fiqh/legal jargon in favor of concrete, everyday
framing (e.g. Hibah explained as "a gift you give right now, while you're
alive" rather than leading with the Arabic term) — written for someone
with no prior estate-planning or Islamic finance background, consistent
with the "convince a 100-year-old grandmother" usability bar already
established for this product.

Wired into: Coverage, Faraid, Assets, Wassiyah, Hibah, Family Waqf,
Nominate, Trigger, Claims (H2), Settings.

e2e-info.cjs: new suite verifying the info button appears, opens a
non-trivial explanation, and closes again on every one of the 10 tabs.
31/31 passing. Re-verified all five prior suites (32/32, 16/16, 12/12,
10/10, 10/10) — 111/111 total, no regressions.
This commit is contained in:
wmj
2026-08-13 18:14:28 +08:00
parent 07b9f4f07a
commit 4d244db6d0
12 changed files with 260 additions and 19 deletions
+49
View File
@@ -0,0 +1,49 @@
// Verifies every tab has a working (i) info button that opens plain-language help.
const { chromium } = require('playwright');
const BASE = 'https://moslem04.falahos.my/';
const results = [];
const consoleErrors = [];
function record(name, pass, detail = '') { results.push({ name, pass, detail }); console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); }
const TABS = ['Coverage', 'Faraid', 'Assets', 'Wassiyah', 'Hibah', 'Family Waqf', 'Nominate', 'Trigger', 'Claims (H2)', 'Settings'];
async function main() {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); });
page.on('pageerror', e => consoleErrors.push(e.message));
await page.goto(BASE, { waitUntil: 'networkidle' });
for (const label of TABS) {
await page.locator('nav button.tab', { hasText: label }).click();
await page.waitForTimeout(200);
const infoBtn = page.locator('.module-header .info-btn');
const btnVisible = await infoBtn.isVisible();
record(`"${label}": info button visible`, btnVisible);
if (!btnVisible) continue;
await infoBtn.click();
await page.waitForTimeout(150);
const panelVisible = await page.locator('.info-panel').isVisible();
const whatText = await page.locator('.info-panel .info-section p').first().textContent().catch(() => '');
record(`"${label}": info panel opens with non-trivial explanation`, panelVisible && whatText.length > 40, `${whatText.length} chars`);
// toggle closed
await infoBtn.click();
await page.waitForTimeout(150);
const panelClosed = await page.locator('.info-panel').isVisible().catch(() => false);
record(`"${label}": info panel closes on second click`, !panelClosed);
}
record('No uncaught JS console errors across all info panels', consoleErrors.length === 0, consoleErrors.join(' || '));
await browser.close();
const passCount = results.filter(r => r.pass).length;
const failCount = results.length - passCount;
console.log(`\n${passCount} passed, ${failCount} failed, ${results.length} total`);
if (failCount > 0) results.filter(r => !r.pass).forEach(r => console.log(` - ${r.name}: ${r.detail}`));
process.exit(failCount > 0 ? 1 : 0);
}
main().catch(e => { console.error('SCRIPT ERROR:', e); process.exit(2); });
+11 -1
View File
@@ -10,6 +10,7 @@
import DigitalClaims from './lib/horizon2/DigitalClaims.svelte'; import DigitalClaims from './lib/horizon2/DigitalClaims.svelte';
import { exportAll, deleteAll } from './lib/storage.js'; import { exportAll, deleteAll } from './lib/storage.js';
import { lang, setLang } from './lib/i18n.js'; import { lang, setLang } from './lib/i18n.js';
import InfoPanel from './lib/InfoPanel.svelte';
let currentLang = $state('en'); let currentLang = $state('en');
lang.subscribe(v => currentLang = v); lang.subscribe(v => currentLang = v);
@@ -73,7 +74,15 @@
{:else if activeTab === 8}<DigitalClaims /> {:else if activeTab === 8}<DigitalClaims />
{:else if activeTab === 9} {:else if activeTab === 9}
<div class="module"> <div class="module">
<h2>Settings</h2> <div class="module-header">
<h2>Settings</h2>
<InfoPanel
title="Settings"
what="Where you control your data and the app's language. Everything you enter in this app stays only on this device — nothing is sent anywhere unless you export it yourself."
how="Switch between English and Bahasa Malaysia here. If you ever want a full backup of everything you've entered, export it. If you want to start completely fresh, delete everything — this cannot be undone, so only use it if you really mean it."
fields={[]}
/>
</div>
<p class="sub">Your data is stored locally on this device. Nothing is sent to a third party.</p> <p class="sub">Your data is stored locally on this device. Nothing is sent to a third party.</p>
<div class="lang-switch"> <div class="lang-switch">
@@ -132,6 +141,7 @@
: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-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; } :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; }
.module-header { display: flex; align-items: center; margin-bottom: 4px; }
.lang-switch { margin-bottom: 16px; } .lang-switch { margin-bottom: 16px; }
.lang-label { display: block; font-size: 12px; color: #B8B2A6; margin-bottom: 8px; } .lang-label { display: block; font-size: 12px; color: #B8B2A6; margin-bottom: 8px; }
.lang-buttons { display: flex; gap: 8px; } .lang-buttons { display: flex; gap: 8px; }
+17 -2
View File
@@ -1,6 +1,7 @@
<script> <script>
import { load, save, estateTotal } from './storage.js'; import { load, save, estateTotal } from './storage.js';
import Disclaimer from './Disclaimer.svelte'; import Disclaimer from './Disclaimer.svelte';
import InfoPanel from './InfoPanel.svelte';
const TYPES = ['Property', 'Cash / Bank', 'Vehicle', 'Business interest', 'Digital assets', 'Jewelry / valuables', 'Other']; const TYPES = ['Property', 'Cash / Bank', 'Vehicle', 'Business interest', 'Digital assets', 'Jewelry / valuables', 'Other'];
@@ -76,7 +77,20 @@
</script> </script>
<div class="module"> <div class="module">
<h2>Asset Registry</h2> <div class="module-header">
<h2>Asset Registry</h2>
<InfoPanel
title="Asset Registry"
what="A simple list of everything you own — your house, bank accounts, car, business, crypto, jewelry, anything of value. This is the foundation everything else in the app builds on: your Faraid shares, wassiyah limit, and coverage percentage are all calculated from what you log here."
how="Add one asset at a time. Start with the big things — property, savings, your car — you can always add smaller items later. Nothing here is submitted anywhere; it just stays on your device."
fields={[
{ label: 'Type', hint: 'What kind of asset it is — property, cash, vehicle, business, digital, jewelry, or other.' },
{ label: 'Description', hint: 'A short name so you recognize it later, e.g. \'Terrace house, Shah Alam\'.' },
{ label: 'Estimated value', hint: 'A rough number is fine — you can update it anytime.' },
{ label: 'Ownership share', hint: 'If you only own part of it (e.g. jointly with a sibling), enter your percentage — otherwise leave at 100%.' }
]}
/>
</div>
<p class="sub">Log what you own — feeds the Faraid Calculator, Wassiyah one-third meter, and Waqf corpus selector.</p> <p class="sub">Log what you own — feeds the Faraid Calculator, Wassiyah one-third meter, and Waqf corpus selector.</p>
<div class="total-card"> <div class="total-card">
@@ -136,7 +150,8 @@
<style> <style>
.module { padding: 4px 0 40px; } .module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; } .module-header { display: flex; align-items: center; margin-bottom: 4px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 0; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; } .sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; }
.total-card { display: flex; justify-content: space-between; align-items: baseline; background: rgba(201,168,76,0.08); border: 1px solid rgba(201,168,76,0.25); border-radius: 12px; padding: 16px; margin-bottom: 18px; } .total-card { display: flex; justify-content: space-between; align-items: baseline; background: rgba(201,168,76,0.08); border: 1px solid rgba(201,168,76,0.25); border-radius: 12px; padding: 16px; margin-bottom: 18px; }
.total-card span { font-size: 12.5px; color: #B8B2A6; } .total-card span { font-size: 12.5px; color: #B8B2A6; }
+12 -2
View File
@@ -4,6 +4,7 @@
// still fall into the slow court queue because no fast-path instrument covers it. // still fall into the slow court queue because no fast-path instrument covers it.
import { computeCoverage, suggestedChannel } from './nonprobate.js'; import { computeCoverage, suggestedChannel } from './nonprobate.js';
import Disclaimer from './Disclaimer.svelte'; import Disclaimer from './Disclaimer.svelte';
import InfoPanel from './InfoPanel.svelte';
const coverage = computeCoverage(); const coverage = computeCoverage();
@@ -29,7 +30,15 @@
</script> </script>
<div class="module"> <div class="module">
<h2>Coverage Dashboard</h2> <div class="module-header">
<h2>Coverage Dashboard</h2>
<InfoPanel
title="Coverage Dashboard"
what="The one number that tells you if this app is actually working for you: what percentage of everything you own will skip the slow Faraid/probate court process entirely. Green means covered and fast; red means it's still stuck in the old system."
how="This updates automatically as you use the other tabs — nothing to fill in here. Just check it, and for anything shown in red, tap the suggested next step to go fix it (usually Hibah, Waqf, or the Nomination Registry)."
fields={[]}
/>
</div>
<p class="sub">What fraction of your estate is already arranged to skip Faraid/probate entirely.</p> <p class="sub">What fraction of your estate is already arranged to skip Faraid/probate entirely.</p>
<div class="big-number-card" class:full={coverage.fastPercent === 100} class:partial={coverage.fastPercent > 0 && coverage.fastPercent < 100} class:none={coverage.fastPercent === 0}> <div class="big-number-card" class:full={coverage.fastPercent === 100} class:partial={coverage.fastPercent > 0 && coverage.fastPercent < 100} class:none={coverage.fastPercent === 0}>
@@ -81,7 +90,8 @@
<style> <style>
.module { padding: 4px 0 40px; } .module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; } .module-header { display: flex; align-items: center; margin-bottom: 4px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 0; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 18px; } .sub { font-size: 13px; color: #8A8478; margin-bottom: 18px; }
.big-number-card { text-align: center; border-radius: 16px; padding: 24px; margin-bottom: 16px; border: 1px solid rgba(255,255,255,0.08); } .big-number-card { text-align: center; border-radius: 16px; padding: 24px; margin-bottom: 16px; border: 1px solid rgba(255,255,255,0.08); }
+16 -2
View File
@@ -17,6 +17,7 @@
// 1-2 week window instead of stacking behind a 2-year probate queue. // 1-2 week window instead of stacking behind a 2-year probate queue.
import { load, save } from './storage.js'; import { load, save } from './storage.js';
import { computeCoverage } from './nonprobate.js'; import { computeCoverage } from './nonprobate.js';
import InfoPanel from './InfoPanel.svelte';
const coverage = computeCoverage(); const coverage = computeCoverage();
@@ -96,7 +97,19 @@
</script> </script>
<div class="module"> <div class="module">
<h2>Death Trigger &amp; Execution</h2> <div class="module-header">
<h2>Death Trigger &amp; Execution</h2>
<InfoPanel
title="Death Trigger & Execution"
what="This is what actually fires when someone passes away. It can't be triggered by one person alone (to prevent fraud or a mistake) — you need at least two trusted people to confirm the death, plus a death certificate reference. Once confirmed, the app generates ready-to-file paperwork for every asset you've already covered, so your family can start claiming things within days instead of waiting on a court."
how="Set this up now, while everyone is healthy — name your executor and at least two attestors (people who will confirm the death when it happens). When the time comes, those attestors log in and confirm, someone enters the death certificate number, and the trigger fires — generating a downloadable packet for each covered asset."
fields={[
{ label: 'Executor', hint: 'The person responsible for carrying out your wishes.' },
{ label: 'Attestors', hint: 'At least two people who will confirm the death actually happened — this is a safety check, not a formality.' },
{ label: 'Date of death & death certificate reference', hint: 'Required before the trigger can fire — this ties everything to an official record.' }
]}
/>
</div>
<p class="sub">Set up now, fires at death. This is the mechanism that turns "arranged" into "distributed" — target: days, not the ~2-year Faraid/probate route.</p> <p class="sub">Set up now, fires at death. This is the mechanism that turns "arranged" into "distributed" — target: days, not the ~2-year Faraid/probate route.</p>
{#if !triggered} {#if !triggered}
@@ -153,7 +166,8 @@
<style> <style>
.module { padding: 4px 0 40px; } .module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; } .module-header { display: flex; align-items: center; margin-bottom: 4px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 0; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 18px; line-height: 1.5; } .sub { font-size: 13px; color: #8A8478; margin-bottom: 18px; line-height: 1.5; }
.setup-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 16px; } .setup-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 16px; }
.setup-card h3 { font-size: 14px; color: #C9A84C; margin: 16px 0 8px; } .setup-card h3 { font-size: 14px; color: #C9A84C; margin: 16px 0 8px; }
+17 -2
View File
@@ -8,6 +8,7 @@
import { load, save, estateTotal } from './storage.js'; import { load, save, estateTotal } from './storage.js';
import MaradAlMawtGuard from './MaradAlMawtGuard.svelte'; import MaradAlMawtGuard from './MaradAlMawtGuard.svelte';
import Disclaimer from './Disclaimer.svelte'; import Disclaimer from './Disclaimer.svelte';
import InfoPanel from './InfoPanel.svelte';
const assets = load('assets', []); const assets = load('assets', []);
const total = estateTotal(assets); const total = estateTotal(assets);
@@ -72,7 +73,20 @@
</script> </script>
<div class="module"> <div class="module">
<h2>Family Waqf Designator</h2> <div class="module-header">
<h2>Family Waqf Designator</h2>
<InfoPanel
title="Family Waqf Designator"
what="A waqf is a permanent endowment — you dedicate an asset (like a piece of land or property) so it can never be sold, and it serves a purpose (usually your family's benefit) forever. Like a hibah, this happens now, while you're alive, so the asset is out of your estate immediately — nothing for a court to process later."
how="Choose an asset from your Asset Registry, name a trustee (mutawalli) who will manage it, and decide how the benefit is shared among your family. There's an open religious question about the one-third limit for this instrument — the app explains it plainly rather than pretending there's a single settled answer."
fields={[
{ label: 'Corpus asset', hint: 'The specific asset you\'re dedicating — pick it from your Asset Registry.' },
{ label: 'Mutawalli (trustee)', hint: 'The person who will manage the asset and make sure it\'s used properly.' },
{ label: 'Successor mutawalli', hint: 'A backup trustee in case the first one can no longer serve.' },
{ label: 'Beneficiaries', hint: 'Who benefits — defaults to an equal split among descendants.' }
]}
/>
</div>
<div class="open-note"> <div class="open-note">
<strong>Open fiqh question:</strong> whether a healthy-state family waqf is subject to a one-third cap has not received scholarly sign-off (tracked as OPEN-01). This module ships without waiting on that review, per explicit product direction — the exported deed says so plainly rather than presenting invented certainty. <strong>Open fiqh question:</strong> whether a healthy-state family waqf is subject to a one-third cap has not received scholarly sign-off (tracked as OPEN-01). This module ships without waiting on that review, per explicit product direction — the exported deed says so plainly rather than presenting invented certainty.
</div> </div>
@@ -130,7 +144,8 @@
<style> <style>
.module { padding: 4px 0 40px; } .module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 10px; } .module-header { display: flex; align-items: center; margin-bottom: 10px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 0; }
.open-note { font-size: 11.5px; color: #C9A84C; background: rgba(201,168,76,0.08); border: 1px solid rgba(201,168,76,0.25); border-radius: 10px; padding: 10px 12px; margin-bottom: 12px; line-height: 1.5; } .open-note { font-size: 11.5px; color: #C9A84C; background: rgba(201,168,76,0.08); border: 1px solid rgba(201,168,76,0.25); border-radius: 10px; padding: 10px 12px; margin-bottom: 12px; line-height: 1.5; }
.fast-path-note { font-size: 11.5px; color: #2ECC71; background: rgba(46,204,113,0.08); border: 1px solid rgba(46,204,113,0.25); border-radius: 10px; padding: 10px 12px; margin-bottom: 12px; line-height: 1.5; } .fast-path-note { font-size: 11.5px; color: #2ECC71; background: rgba(46,204,113,0.08); border: 1px solid rgba(46,204,113,0.25); border-radius: 10px; padding: 10px 12px; margin-bottom: 12px; line-height: 1.5; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; } .sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; }
+17 -2
View File
@@ -1,6 +1,7 @@
<script> <script>
import { calculateFaraid, applyEstateValue } from './calc/faraid.js'; import { calculateFaraid, applyEstateValue } from './calc/faraid.js';
import Disclaimer from './Disclaimer.svelte'; import Disclaimer from './Disclaimer.svelte';
import InfoPanel from './InfoPanel.svelte';
let deceasedGender = $state('male'); let deceasedGender = $state('male');
let estateValue = $state(0); let estateValue = $state(0);
@@ -28,7 +29,20 @@
</script> </script>
<div class="module"> <div class="module">
<h2>Faraid Calculator</h2> <div class="module-header">
<h2>Faraid Calculator</h2>
<InfoPanel
title="Faraid Calculator"
what="This works out the fixed Islamic inheritance shares — who in your family is legally entitled to what, according to the Quran. Think of it as 'if this asset ends up going through the normal court process, here's how it would be split.'"
how="Answer simple questions about who in your family is still alive: spouse, children, parents, siblings. The calculator does the math instantly — no need to know any religious terminology."
fields={[
{ label: 'Deceased\'s gender', hint: 'Whose estate is being calculated.' },
{ label: 'Estate value', hint: 'Optional — enter a number if you want to see actual amounts, not just fractions.' },
{ label: 'Wives / husband, sons, daughters, father, mother', hint: 'Tick or enter a count for whoever is still alive.' },
{ label: 'Siblings', hint: 'Only asked if there is no surviving father or son — otherwise siblings don\'t inherit.' }
]}
/>
</div>
<div class="reframe-note">This calculator is informational — it shows what would apply to whatever is <em>left</em> in your estate. It is not itself a fast-path mechanism: anything still in your estate at death goes through the conventional Faraid/probate process. Check the Coverage Dashboard and use Hibah/Waqf/Nomination to move assets out of this calculation entirely.</div> <div class="reframe-note">This calculator is informational — it shows what would apply to whatever is <em>left</em> in your estate. It is not itself a fast-path mechanism: anything still in your estate at death goes through the conventional Faraid/probate process. Check the Coverage Dashboard and use Hibah/Waqf/Nomination to move assets out of this calculation entirely.</div>
<p class="sub">Answer simple questions about surviving family — no fiqh terminology needed.</p> <p class="sub">Answer simple questions about surviving family — no fiqh terminology needed.</p>
@@ -96,7 +110,8 @@
<style> <style>
.module { padding: 4px 0 40px; } .module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; } .module-header { display: flex; align-items: center; margin-bottom: 4px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 0; }
.reframe-note { font-size: 11.5px; color: #B8B2A6; background: rgba(255,255,255,0.04); border-radius: 10px; padding: 10px 12px; margin: 10px 0 12px; line-height: 1.5; } .reframe-note { font-size: 11.5px; color: #B8B2A6; background: rgba(255,255,255,0.04); border-radius: 10px; padding: 10px 12px; margin: 10px 0 12px; line-height: 1.5; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 20px; } .sub { font-size: 13px; color: #8A8478; margin-bottom: 20px; }
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; } .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
+17 -2
View File
@@ -2,6 +2,7 @@
import { load, save, estateTotal } from './storage.js'; import { load, save, estateTotal } from './storage.js';
import MaradAlMawtGuard from './MaradAlMawtGuard.svelte'; import MaradAlMawtGuard from './MaradAlMawtGuard.svelte';
import Disclaimer from './Disclaimer.svelte'; import Disclaimer from './Disclaimer.svelte';
import InfoPanel from './InfoPanel.svelte';
const assets = load('assets', []); const assets = load('assets', []);
const total = estateTotal(assets); const total = estateTotal(assets);
@@ -47,7 +48,20 @@
</script> </script>
<div class="module"> <div class="module">
<h2>Hibah Tracker</h2> <div class="module-header">
<h2>Hibah Tracker</h2>
<InfoPanel
title="Hibah Tracker"
what="A hibah is simply a gift you give right now, while you're alive — not something that happens after you die. This is the single most powerful tool in this app: once a gift is properly completed, that asset belongs to the recipient today, and it's no longer part of your estate at all — so there's nothing left for a court to fight over or take years to process."
how="Pick something you own, decide who you want to give it to, and log it here. Link it to the matching entry in your Asset Registry so the app knows it's covered. If you're seriously unwell when you make the gift, a special rule kicks in (the marad al-mawt question) — just answer honestly, the app handles the rest."
fields={[
{ label: 'Recipient & relation', hint: 'Who you\'re giving it to and how they\'re related to you.' },
{ label: 'Asset / gift description', hint: 'What you\'re giving — try to match the wording in your Asset Registry.' },
{ label: 'Link to a registered asset', hint: 'This is what moves it out of your probate-bound estate on the Coverage Dashboard.' },
{ label: 'Offer-and-acceptance statement', hint: 'A short sentence confirming the recipient has accepted the gift.' }
]}
/>
</div>
<div class="fast-path-note">This is a fast-path instrument. Once offer, acceptance, and possession are complete, this asset belongs to the recipient now — it is no longer part of your estate, so there is nothing for a Faraid/probate court to adjudicate on it at death.</div> <div class="fast-path-note">This is a fast-path instrument. Once offer, acceptance, and possession are complete, this asset belongs to the recipient now — it is no longer part of your estate, so there is nothing for a Faraid/probate court to adjudicate on it at death.</div>
<p class="sub">Log a lifetime gift — completed by offer, acceptance, and possession.</p> <p class="sub">Log a lifetime gift — completed by offer, acceptance, and possession.</p>
@@ -100,7 +114,8 @@
<style> <style>
.module { padding: 4px 0 40px; } .module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; } .module-header { display: flex; align-items: center; margin-bottom: 4px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 0; }
.fast-path-note { font-size: 11.5px; color: #2ECC71; background: rgba(46,204,113,0.08); border: 1px solid rgba(46,204,113,0.25); border-radius: 10px; padding: 10px 12px; margin-bottom: 12px; line-height: 1.5; } .fast-path-note { font-size: 11.5px; color: #2ECC71; background: rgba(46,204,113,0.08); border: 1px solid rgba(46,204,113,0.25); border-radius: 10px; padding: 10px 12px; margin-bottom: 12px; line-height: 1.5; }
.fast-badge { display: inline-block; margin-top: 4px; margin-left: 6px; font-size: 10.5px; background: #2ECC71; color: #070A0D; padding: 2px 8px; border-radius: 6px; font-weight: 600; } .fast-badge { display: inline-block; margin-top: 4px; margin-left: 6px; font-size: 10.5px; background: #2ECC71; color: #070A0D; padding: 2px 8px; border-radius: 6px; font-weight: 600; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; } .sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; }
+55
View File
@@ -0,0 +1,55 @@
<script>
// Shared (i) info button + expandable plain-language explainer. One component,
// every module's header uses it — so tone and behavior stay consistent instead
// of each tab inventing its own help text pattern.
let { title, what = '', how = '', fields = [] } = $props();
let open = $state(false);
</script>
<button class="info-btn" onclick={() => open = !open} aria-label="What is this tab?" aria-expanded={open}>
<span class="info-icon">i</span>
</button>
{#if open}
<div class="info-panel">
<div class="info-section">
<span class="info-label">What this is</span>
<p>{what}</p>
</div>
{#if how}
<div class="info-section">
<span class="info-label">How to use it</span>
<p>{how}</p>
</div>
{/if}
{#if fields.length}
<div class="info-section">
<span class="info-label">What to enter</span>
<ul>
{#each fields as f}<li><strong>{f.label}:</strong> {f.hint}</li>{/each}
</ul>
</div>
{/if}
</div>
{/if}
<style>
.info-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 22px; height: 22px; border-radius: 50%;
background: rgba(201,168,76,0.15); border: 1px solid rgba(201,168,76,0.35);
cursor: pointer; margin-left: 8px; vertical-align: middle; padding: 0;
}
.info-icon { font-family: 'DM Serif Display', serif; font-style: italic; font-size: 13px; color: #C9A84C; line-height: 1; }
.info-panel {
background: rgba(201,168,76,0.06); border: 1px solid rgba(201,168,76,0.2);
border-radius: 12px; padding: 14px; margin: 10px 0 16px;
}
.info-section { margin-bottom: 10px; }
.info-section:last-child { margin-bottom: 0; }
.info-label { display: block; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.5px; color: #C9A84C; font-weight: 700; margin-bottom: 4px; }
.info-panel p { font-size: 12.5px; color: #E8E4DC; line-height: 1.6; }
.info-panel ul { margin: 0; padding-left: 18px; }
.info-panel li { font-size: 12.5px; color: #E8E4DC; line-height: 1.6; margin-bottom: 4px; }
.info-panel li strong { color: #C9A84C; }
</style>
+16 -2
View File
@@ -12,6 +12,7 @@
import { load, save } from './storage.js'; import { load, save } from './storage.js';
import { suggestedChannel } from './nonprobate.js'; import { suggestedChannel } from './nonprobate.js';
import Disclaimer from './Disclaimer.svelte'; import Disclaimer from './Disclaimer.svelte';
import InfoPanel from './InfoPanel.svelte';
const assets = load('assets', []); const assets = load('assets', []);
@@ -165,7 +166,19 @@
</script> </script>
<div class="module"> <div class="module">
<h2>Nomination Registry</h2> <div class="module-header">
<h2>Nomination Registry</h2>
<InfoPanel
title="Nomination Registry"
what="Some things can't be simply gifted away (like your EPF/retirement savings, insurance, or a business) — but many of them have their own built-in shortcut that pays your chosen person directly, without needing a court at all. This tab is where you record which shortcut covers which asset."
how="Pick an asset from your registry, and the app suggests the right shortcut for that type — EPF/Takaful nomination for retirement and insurance money, a bank mandate for cash, a trust for land, a business continuity instrument for a company, or a digital custody plan for crypto. Fill in the fields for that shortcut and save it — then remember to actually file the real nomination with that institution too, this app only keeps a record."
fields={[
{ label: 'Asset', hint: 'Pick from your Asset Registry — the app will suggest the right channel automatically.' },
{ label: 'Channel type', hint: 'EPF, Takaful/insurance, bank mandate, trust, business continuity, or digital custody — pick what fits.' },
{ label: 'Extra fields', hint: 'Change depending on the channel — e.g. a trustee for a trust, or a nominee for EPF/Takaful.' }
]}
/>
</div>
<div class="fast-path-note">Third fast-path channel. EPF and Takaful/insurance nominations pay the nominee directly by law — no Faraid/probate involvement at all. Bank mandates and trust/nominee holdings need setup now, while alive, but also bypass the court queue once in place.</div> <div class="fast-path-note">Third fast-path channel. EPF and Takaful/insurance nominations pay the nominee directly by law — no Faraid/probate involvement at all. Bank mandates and trust/nominee holdings need setup now, while alive, but also bypass the court queue once in place.</div>
<p class="sub">Log which non-probate channel covers each asset that can't be fully gifted (Hibah) or dedicated (Waqf).</p> <p class="sub">Log which non-probate channel covers each asset that can't be fully gifted (Hibah) or dedicated (Waqf).</p>
@@ -276,7 +289,8 @@
<style> <style>
.module { padding: 4px 0 40px; } .module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; } .module-header { display: flex; align-items: center; margin-bottom: 4px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 0; }
.fast-path-note { font-size: 11.5px; color: #2ECC71; background: rgba(46,204,113,0.08); border: 1px solid rgba(46,204,113,0.25); border-radius: 10px; padding: 10px 12px; margin: 10px 0 12px; line-height: 1.5; } .fast-path-note { font-size: 11.5px; color: #2ECC71; background: rgba(46,204,113,0.08); border: 1px solid rgba(46,204,113,0.25); border-radius: 10px; padding: 10px 12px; margin: 10px 0 12px; line-height: 1.5; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; } .sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; }
.form-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 16px; } .form-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 16px; }
+17 -2
View File
@@ -2,6 +2,7 @@
import { load, save, estateTotal } from './storage.js'; import { load, save, estateTotal } from './storage.js';
import { oneThirdCap, isQuranicHeirRelation } from './calc/faraid.js'; import { oneThirdCap, isQuranicHeirRelation } from './calc/faraid.js';
import Disclaimer from './Disclaimer.svelte'; import Disclaimer from './Disclaimer.svelte';
import InfoPanel from './InfoPanel.svelte';
const assets = load('assets', []); const assets = load('assets', []);
const total = estateTotal(assets); const total = estateTotal(assets);
@@ -67,7 +68,20 @@
</script> </script>
<div class="module"> <div class="module">
<h2>Wassiyah Generator</h2> <div class="module-header">
<h2>Wassiyah Generator</h2>
<InfoPanel
title="Wassiyah Generator"
what="A wassiyah is a will — but in Islam it can only cover up to one-third of what you own, and it can't be left to people who already automatically inherit (like your spouse, children, or parents). It's for the people you'd otherwise leave nothing to: a charity, a friend, a nephew."
how="Important: this does NOT skip the slow court process — a wassiyah still goes through it. If your goal is fast distribution, use Hibah or Waqf instead for those specific assets. Use this tab only for the one-third portion you specifically want handled as a will."
fields={[
{ label: 'Jurisdiction', hint: 'UK or Malaysia — changes what legal notes appear on the exported document.' },
{ label: 'Recipient name & relation', hint: 'Who gets it and how they\'re related to you — must NOT be someone who already inherits automatically.' },
{ label: 'Description & value', hint: 'What you\'re leaving them and roughly how much it\'s worth.' },
{ label: 'Witnesses', hint: 'Two people who will witness your will, as required by Islamic and civil law.' }
]}
/>
</div>
<div class="slow-path-warning"> <div class="slow-path-warning">
<strong>This does not bypass Faraid/probate.</strong> A wassiyah is a post-death instrument — the assets it covers are still part of your estate at death and still go through the conventional Faraid/probate process to be executed. If your goal is distribution within 1-2 weeks of death, use <strong>Hibah</strong> (lifetime gift) or <strong>Waqf</strong> for those assets instead — a wassiyah is only appropriate for the discretionary one-third you specifically want administered through the will process (e.g. bequests to non-heirs), not as a fast-track vehicle. <strong>This does not bypass Faraid/probate.</strong> A wassiyah is a post-death instrument — the assets it covers are still part of your estate at death and still go through the conventional Faraid/probate process to be executed. If your goal is distribution within 1-2 weeks of death, use <strong>Hibah</strong> (lifetime gift) or <strong>Waqf</strong> for those assets instead — a wassiyah is only appropriate for the discretionary one-third you specifically want administered through the will process (e.g. bequests to non-heirs), not as a fast-track vehicle.
</div> </div>
@@ -120,7 +134,8 @@
<style> <style>
.module { padding: 4px 0 40px; } .module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; } .module-header { display: flex; align-items: center; margin-bottom: 4px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 0; }
.slow-path-warning { font-size: 11.5px; color: #EF4444; background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.25); border-radius: 10px; padding: 10px 12px; margin: 10px 0 12px; line-height: 1.5; } .slow-path-warning { font-size: 11.5px; color: #EF4444; background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.25); border-radius: 10px; padding: 10px 12px; margin: 10px 0 12px; line-height: 1.5; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; } .sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; }
.meter-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 18px; } .meter-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 18px; }
+16 -2
View File
@@ -5,6 +5,7 @@
// tradeable security or a live institutional integration, and it is neither. // tradeable security or a live institutional integration, and it is neither.
import { listClaims, issueClaim, transferClaim, removeClaim } from './claims.js'; import { listClaims, issueClaim, transferClaim, removeClaim } from './claims.js';
import Disclaimer from '../Disclaimer.svelte'; import Disclaimer from '../Disclaimer.svelte';
import InfoPanel from '../InfoPanel.svelte';
let claims = $state(listClaims()); let claims = $state(listClaims());
let form = $state({ assetLabel: '', heirPoolId: '', holderName: '', heirShareFraction: '', faraidRulingRef: '' }); let form = $state({ assetLabel: '', heirPoolId: '', holderName: '', heirShareFraction: '', faraidRulingRef: '' });
@@ -45,7 +46,19 @@
SPV/trust legal wrapper or institutional partner. SPV/trust legal wrapper or institutional partner.
</div> </div>
<h2>Digital Beneficial Claims</h2> <div class="module-header">
<h2>Digital Beneficial Claims</h2>
<InfoPanel
title="Digital Beneficial Claims"
what="This is a demo, not a real, live system yet. The idea: if an estate is already frozen (stuck in the old court process for years), each heir's already-calculated share of a locked asset — like land — could be represented as a claim they can transfer to another heir or cash out, instead of everyone being stuck waiting for the whole thing to resolve together."
how="Nothing you do here has real legal effect yet — it needs a real legal entity and an institutional partner behind it first. Use it to see how the idea would work: issue a demo claim, then try transferring it (only allowed within the same family/heir group, never to a stranger)."
fields={[
{ label: 'Asset', hint: 'The specific locked/frozen asset, e.g. a piece of land.' },
{ label: 'Heir pool ID', hint: 'A shared code for everyone who co-inherits this asset.' },
{ label: 'Holder name & share fraction', hint: 'Who currently holds the claim and what fraction of the asset it represents.' }
]}
/>
</div>
<p class="sub">Represents one heir's already-calculated, faraid-verified fractional interest in a specific illiquid asset. Transferable only within the same heir pool, or back to the wrapper entity — never onto an open market.</p> <p class="sub">Represents one heir's already-calculated, faraid-verified fractional interest in a specific illiquid asset. Transferable only within the same heir pool, or back to the wrapper entity — never onto an open market.</p>
<div class="form-card"> <div class="form-card">
@@ -91,7 +104,8 @@
<style> <style>
.module { padding: 4px 0 40px; } .module { padding: 4px 0 40px; }
.pilot-banner { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 10px; padding: 12px 14px; font-size: 11.5px; color: #E8E4DC; line-height: 1.5; margin-bottom: 18px; } .pilot-banner { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 10px; padding: 12px 14px; font-size: 11.5px; color: #E8E4DC; line-height: 1.5; margin-bottom: 18px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; } .module-header { display: flex; align-items: center; margin-bottom: 4px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 0; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; } .sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; }
.form-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 16px; } .form-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 16px; }
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }