diff --git a/public/assets/fonts/ArabicYekanXRegular.ttf b/public/assets/fonts/ArabicYekanXRegular.ttf
new file mode 100644
index 0000000..cb01293
Binary files /dev/null and b/public/assets/fonts/ArabicYekanXRegular.ttf differ
diff --git a/public/assets/fonts/Faminela.otf b/public/assets/fonts/Faminela.otf
new file mode 100644
index 0000000..091ff77
Binary files /dev/null and b/public/assets/fonts/Faminela.otf differ
diff --git a/public/assets/fonts/NotoNastaliqUrdu.ttf b/public/assets/fonts/NotoNastaliqUrdu.ttf
new file mode 100644
index 0000000..3362081
Binary files /dev/null and b/public/assets/fonts/NotoNastaliqUrdu.ttf differ
diff --git a/public/assets/fonts/SegoeUIBold.ttf b/public/assets/fonts/SegoeUIBold.ttf
new file mode 100644
index 0000000..5723e8b
Binary files /dev/null and b/public/assets/fonts/SegoeUIBold.ttf differ
diff --git a/public/assets/fonts/YekanXFaNum-R.ttf b/public/assets/fonts/YekanXFaNum-R.ttf
new file mode 100644
index 0000000..c96391a
Binary files /dev/null and b/public/assets/fonts/YekanXFaNum-R.ttf differ
diff --git a/public/assets/fonts/faminela.ttf b/public/assets/fonts/faminela.ttf
new file mode 100644
index 0000000..a99d4f2
Binary files /dev/null and b/public/assets/fonts/faminela.ttf differ
diff --git a/public/assets/fonts/segoeui.ttf b/public/assets/fonts/segoeui.ttf
new file mode 100644
index 0000000..0f52cbd
Binary files /dev/null and b/public/assets/fonts/segoeui.ttf differ
diff --git a/src/app/[lang]/layout.tsx b/src/app/[lang]/layout.tsx
index e8556bd..a131730 100644
--- a/src/app/[lang]/layout.tsx
+++ b/src/app/[lang]/layout.tsx
@@ -21,7 +21,13 @@ export default async function LocaleLayout({
return (
- {children}
+
+ {children}
+
);
}
diff --git a/src/app/globals.css b/src/app/globals.css
index b757cbe..6ca0a07 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,5 +1,49 @@
@import "tailwindcss";
+/* ─── App fonts matching Flutter Najm & Hosseinieh ─── */
+@font-face {
+ font-family: 'Faminela';
+ src: url('/assets/fonts/faminela.ttf') format('truetype');
+ font-weight: 400 900;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'YekanX';
+ src: url('/assets/fonts/YekanXFaNum-R.ttf') format('truetype');
+ font-weight: 400 900;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'YekanXArabic';
+ src: url('/assets/fonts/ArabicYekanXRegular.ttf') format('truetype');
+ font-weight: 400 900;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'NastaliqUrdu';
+ src: url('/assets/fonts/NotoNastaliqUrdu.ttf') format('truetype');
+ font-weight: 400 900;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'Segoeui';
+ src: url('/assets/fonts/segoeui.ttf') format('truetype');
+ font-weight: 400 500;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'Segoeui';
+ src: url('/assets/fonts/SegoeUIBold.ttf') format('truetype');
+ font-weight: 600 900;
+ font-style: normal;
+ font-display: swap;
+}
+
:root {
--background: #F5F5F5;
--default-page-background-image: url("/assets/images/home-Checkups-List.svg");
@@ -8,6 +52,10 @@
--safe-left: env(safe-area-inset-left, 0px);
--safe-right: env(safe-area-inset-right, 0px);
+ /* Default body font follows Flutter AppTheme: Segoeui for non-RTL app locales */
+ --marriage-font-sans: 'Segoeui', 'YekanXArabic', 'YekanX', system-ui, sans-serif;
+ --marriage-font-brand: 'Faminela', 'Segoeui', 'YekanXArabic', 'YekanX', system-ui, sans-serif;
+
/* ─── Semantic Tokens (Light) ─── */
--semantic-neutral-bg: #f1f5f9;
--semantic-neutral-text: #475569;
@@ -72,16 +120,44 @@
--semantic-ai-icon: #c4b5fd;
}
+html[lang='fa'],
+body[data-lang='fa'],
+.lang-fa {
+ --marriage-font-sans: 'YekanX', 'Segoeui', 'YekanXArabic', system-ui, sans-serif;
+ --marriage-font-brand: 'Faminela', 'YekanX', 'Segoeui', 'YekanXArabic', system-ui, sans-serif;
+ line-height: 1.5;
+}
+
+html[lang='ar'],
+body[data-lang='ar'],
+.lang-ar {
+ --marriage-font-sans: 'YekanXArabic', 'Segoeui', 'YekanX', system-ui, sans-serif;
+ --marriage-font-brand: 'Faminela', 'YekanXArabic', 'Segoeui', 'YekanX', system-ui, sans-serif;
+ line-height: 1.5;
+}
+
+html[lang='ur'],
+body[data-lang='ur'],
+.lang-ur,
+html[lang='ks'],
+body[data-lang='ks'],
+.lang-ks {
+ --marriage-font-sans: 'NastaliqUrdu', 'Segoeui', 'YekanXArabic', 'YekanX', system-ui, sans-serif;
+ --marriage-font-brand: 'Faminela', 'NastaliqUrdu', 'Segoeui', 'YekanXArabic', 'YekanX', system-ui, sans-serif;
+ line-height: 2;
+}
+
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
- --font-sans: var(--font-segoeui), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+ --font-sans: var(--marriage-font-sans);
+ --font-brand: var(--marriage-font-brand);
+ --font-faminela: var(--marriage-font-brand);
--font-mono: var(--font-geist-mono);
- --font-arabic: var(--font-yekanx-arabic), var(--font-segoeui), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
- --font-persian: var(--font-yekanx), var(--font-yekanx-arabic), var(--font-segoeui), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
- --font-urdu: var(--font-nastaliq-urdu), var(--font-yekanx-arabic), var(--font-segoeui), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
- --font-ryling: var(--font-segoeui), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
- --font-faminela: var(--font-faminela-local), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+ --font-arabic: 'YekanXArabic', 'Segoeui', 'YekanX', system-ui, sans-serif;
+ --font-persian: 'YekanX', 'Segoeui', 'YekanXArabic', system-ui, sans-serif;
+ --font-urdu: 'NastaliqUrdu', 'Segoeui', 'YekanXArabic', 'YekanX', system-ui, sans-serif;
+ --font-ryling: var(--marriage-font-sans);
--color-semantic-neutral-bg: var(--semantic-neutral-bg);
--color-semantic-neutral-text: var(--semantic-neutral-text);
@@ -190,6 +266,27 @@ textarea,
user-select: text !important;
}
+html,
+body,
+button,
+input,
+optgroup,
+select,
+textarea,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+span,
+a,
+label,
+.app-shell {
+ font-family: var(--marriage-font-sans);
+}
+
body {
width: 100%;
height: 100%;
@@ -198,49 +295,26 @@ body {
justify-content: center;
overflow: hidden;
color: var(--foreground);
- font-family: var(--font-segoeui), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
overscroll-behavior: none;
+ font-size: 14px;
+ word-spacing: 0.25px;
}
-/* ─── Multi-Language Typography matching Flutter Najm AppTheme ─── */
-/* 1. Arabic (ar) -> Flutter yekanxArabic (ArabicYekanXRegular.ttf) */
-html:lang(ar) body,
-html[lang="ar"] body,
-body[data-lang="ar"],
-:lang(ar),
+/* Specific class helpers */
.font-arabic {
- font-family: var(--font-yekanx-arabic), var(--font-segoeui), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+ font-family: 'YekanXArabic', 'Segoeui', 'YekanX', system-ui, sans-serif;
}
-/* 2. Persian (fa) -> Flutter yekanx (YekanXFaNum-R.ttf) */
-html:lang(fa) body,
-html[lang="fa"] body,
-body[data-lang="fa"],
-:lang(fa),
.font-persian {
- font-family: var(--font-yekanx), var(--font-yekanx-arabic), var(--font-segoeui), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
-}
-
-/* 3. Urdu (ur) & Kashmiri (ks) -> Flutter NastaliqUrdu (NotoNastaliqUrdu.ttf) */
-html:lang(ur) body,
-html[lang="ur"] body,
-body[data-lang="ur"],
-:lang(ur),
-html:lang(ks) body,
-html[lang="ks"] body,
-body[data-lang="ks"],
-:lang(ks),
-.font-urdu {
- font-family: var(--font-nastaliq-urdu), var(--font-yekanx-arabic), var(--font-segoeui), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+ font-family: 'YekanX', 'Segoeui', 'YekanXArabic', system-ui, sans-serif;
}
-/* 4. Global / Latin / Cyrillic / Asian languages -> Flutter Segoeui */
-html:not([lang="ar"]):not([lang="fa"]):not([lang="ur"]):not([lang="ks"]) body {
- font-family: var(--font-segoeui), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+.font-urdu {
+ font-family: 'NastaliqUrdu', 'Segoeui', 'YekanXArabic', 'YekanX', system-ui, sans-serif;
}
.font-faminela {
- font-family: var(--font-faminela-local), var(--font-segoeui), "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+ font-family: var(--marriage-font-brand);
}
.app-shell {
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index ca02791..b820a6d 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,5 +1,4 @@
import type { Metadata, Viewport } from "next";
-import localFont from "next/font/local";
import { cookies, headers } from "next/headers";
import Providers from "./providers";
import "./globals.css";
@@ -13,57 +12,6 @@ import {
import { getInitialMarriageProfile } from "@/hooks/marriage/use-profile-main";
import type { MarriageProfileResponse } from "@/hooks/marriage/types";
-const yekanxArabic = localFont({
- src: "../../public/fonts/yekanx-arabic/ArabicYekanXRegular.ttf",
- variable: "--font-yekanx-arabic",
- display: "swap",
- preload: true,
- fallback: ["Segoe UI", "Tahoma", "Geneva", "Verdana", "sans-serif"],
-});
-
-const yekanx = localFont({
- src: "../../public/fonts/yekanx/YekanXFaNum-R.ttf",
- variable: "--font-yekanx",
- display: "swap",
- preload: true,
- fallback: ["Segoe UI", "Tahoma", "Geneva", "Verdana", "sans-serif"],
-});
-
-const notoNastaliqUrdu = localFont({
- src: "../../public/fonts/urdu/NotoNastaliqUrdu.ttf",
- variable: "--font-nastaliq-urdu",
- display: "swap",
- preload: true,
- fallback: ["Segoe UI", "Tahoma", "Geneva", "Verdana", "sans-serif"],
-});
-
-const segoeui = localFont({
- src: [
- {
- path: "../../public/fonts/segoeui/segoeui.ttf",
- weight: "400",
- style: "normal",
- },
- {
- path: "../../public/fonts/segoeui/SegoeUIBold.ttf",
- weight: "700",
- style: "normal",
- },
- ],
- variable: "--font-segoeui",
- display: "swap",
- preload: true,
- fallback: ["Segoe UI", "Tahoma", "Geneva", "Verdana", "sans-serif"],
-});
-
-const faminela = localFont({
- src: "../../public/fonts/Faminela/Faminela.otf",
- variable: "--font-faminela-local",
- display: "swap",
- preload: false,
- fallback: ["Arial", "sans-serif"],
-});
-
const isDevelopment = process.env.NODE_ENV !== "production";
export const metadata: Metadata = {
@@ -420,10 +368,7 @@ export default async function RootLayout({
}}
/>
-
+
{isDevelopment ? : null}
{children}
diff --git a/src/components/Componentes/information-sheet.test.tsx b/src/components/Componentes/information-sheet.test.tsx
index 66ab292..43a6b8c 100644
--- a/src/components/Componentes/information-sheet.test.tsx
+++ b/src/components/Componentes/information-sheet.test.tsx
@@ -1,8 +1,7 @@
-import { render, screen, fireEvent, cleanup, act } from "@testing-library/react";
-import { describe, expect, it, vi, afterEach, beforeEach } from "vitest";
+import { render, screen, fireEvent, cleanup, act, waitFor } from "@testing-library/react";
+import { describe, expect, it, vi, afterEach } from "vitest";
import React from "react";
import { InformationSheet } from "./information-sheet";
-import { viewPaddingsBridge } from "@/lib/view-paddings";
describe("InformationSheet", () => {
afterEach(() => {
@@ -22,7 +21,7 @@ describe("InformationSheet", () => {
expect(screen.getByText("Test Sheet Description")).toBeDefined();
});
- it("should apply smooth keyboard lift when an input inside the sheet receives focus and keyboard height is set", () => {
+ it("should apply smooth keyboard lift when an input inside the sheet receives focus and keyboard height is set", async () => {
render(
{
const section = document.querySelector("section");
expect(section).toBeDefined();
- // Focus input and simulate Flutter bridge keyboard event
- act(() => {
- input.focus();
- fireEvent.focusIn(input);
- // Simulate flutter keyboard bridge event
- window.dispatchEvent(
- new MessageEvent("message", {
- data: JSON.stringify({
- action: "keyboard_changed",
- height: 280,
- }),
+ if (section) {
+ vi.spyOn(section, "getBoundingClientRect").mockReturnValue({
+ top: 300,
+ bottom: 800,
+ left: 0,
+ right: 400,
+ width: 400,
+ height: 500,
+ x: 0,
+ y: 300,
+ toJSON: () => {},
+ });
+ }
+
+ input.focus();
+ fireEvent.focusIn(input);
+ window.dispatchEvent(
+ new MessageEvent("message", {
+ data: JSON.stringify({
+ action: "keyboard_changed",
+ height: 280,
}),
- );
- });
+ }),
+ );
- expect(section?.style.marginBottom).toBe("280px");
+ await waitFor(() => {
+ expect(section?.style.transform).toContain("translate3d");
+ });
});
- it("should reset keyboard lift when input loses focus", async () => {
+ it("should reset keyboard lift when keyboard closes or input loses focus", async () => {
render(
{
const input = screen.getByTestId("test-input");
const section = document.querySelector("section");
- act(() => {
- input.focus();
- fireEvent.focusIn(input);
- });
+ input.focus();
+ fireEvent.focusIn(input);
- act(() => {
- input.blur();
- fireEvent.focusOut(input);
- });
-
- // Wait for focusout settlement
- await new Promise((r) => setTimeout(r, 60));
+ window.dispatchEvent(
+ new MessageEvent("message", {
+ data: JSON.stringify({
+ action: "keyboard_changed",
+ height: 0,
+ }),
+ }),
+ );
- expect(section?.style.marginBottom).toBe("");
+ await waitFor(() => {
+ expect(section?.style.transform).toBe("");
+ });
});
});
+
diff --git a/src/components/Componentes/information-sheet.tsx b/src/components/Componentes/information-sheet.tsx
index 5d3b1ae..60be3ce 100644
--- a/src/components/Componentes/information-sheet.tsx
+++ b/src/components/Componentes/information-sheet.tsx
@@ -328,35 +328,70 @@ export function InformationSheet({
return;
}
- let bridgeKbHeight = viewPaddingsBridge.getConfig().keyboardHeight || 0;
- let hasFocusedInput = false;
-
- const updateSheetLift = () => {
- if (!sheetRef.current) return;
-
- const visualViewport = window.visualViewport;
- const vpReduction = visualViewport
- ? Math.max(0, window.innerHeight - visualViewport.height)
- : 0;
-
- const detectedHeight = Math.max(bridgeKbHeight, vpReduction);
- const defaultHeight =
- typeof window !== "undefined" && window.innerHeight > 0
- ? Math.round(window.innerHeight * 0.375)
- : 285;
-
- const effectiveHeight =
- detectedHeight > 20
- ? detectedHeight
- : hasFocusedInput
- ? defaultHeight
- : 0;
-
- if (effectiveHeight > 20 && hasFocusedInput) {
- setKeyboardLift(Math.round(effectiveHeight));
- } else if (!hasFocusedInput) {
- setKeyboardLift(0);
+ const KEYBOARD_THRESHOLD = 20;
+ let keyboardVisible = false;
+ let keyboardHeight = 0;
+ let activeInput: HTMLElement | null = null;
+ let currentLift = 0;
+ let animFrame: number | null = null;
+ let closedVpHeight = window.visualViewport?.height ?? window.innerHeight;
+
+ const setLift = (nextLift: number) => {
+ currentLift = Math.max(0, Math.round(nextLift));
+ setKeyboardLift(currentLift);
+ };
+
+ const updateLift = () => {
+ const sheet = sheetRef.current;
+ if (!sheet || !keyboardVisible || !activeInput) {
+ setLift(0);
+ return;
}
+
+ // 1. Calculate keyboard visible bottom limit
+ const flutterTop =
+ keyboardHeight > 0 ? window.innerHeight - keyboardHeight : null;
+ const viewport = window.visualViewport;
+ const viewportBottom = viewport
+ ? viewport.offsetTop + viewport.height
+ : window.innerHeight;
+ const visibleBottom =
+ flutterTop !== null
+ ? Math.min(flutterTop, viewportBottom)
+ : viewportBottom;
+
+ // 2. Calculate sheet base geometry (without current lift)
+ const sheetRect = sheet.getBoundingClientRect();
+ const baseBottom = sheetRect.bottom + currentLift;
+ const baseTop = sheetRect.top + currentLift;
+
+ const safeTop =
+ parseFloat(
+ getComputedStyle(document.documentElement).getPropertyValue(
+ "--safe-top",
+ ) || "0",
+ ) || 16;
+ const visibleTop = safeTop + 8;
+
+ // 3. Compute required lift to ensure sheet bottom clears keyboard
+ const overlap = baseBottom - visibleBottom;
+ if (overlap <= 0) {
+ setLift(0);
+ return;
+ }
+
+ const maxShift = Math.max(0, baseTop - visibleTop);
+ const computedLift = Math.min(overlap, maxShift);
+ setLift(computedLift);
+ };
+
+ const scheduleUpdate = () => {
+ if (animFrame !== null) cancelAnimationFrame(animFrame);
+ animFrame = requestAnimationFrame(() => {
+ animFrame = null;
+ updateLift();
+ });
+ updateLift();
};
const handleFocusIn = (e: FocusEvent) => {
@@ -364,45 +399,89 @@ export function InformationSheet({
sheetRef.current?.contains(e.target as Node) &&
isKeyboardInputTarget(e.target)
) {
- hasFocusedInput = true;
- updateSheetLift();
- setTimeout(() => {
- if (e.target instanceof HTMLElement) {
- e.target.scrollIntoView({ behavior: "smooth", block: "center" });
- }
- }, 150);
+ activeInput = e.target;
+ const currentVpHeight =
+ window.visualViewport?.height ?? window.innerHeight;
+ if (
+ keyboardHeight > 0 ||
+ closedVpHeight - currentVpHeight > KEYBOARD_THRESHOLD
+ ) {
+ keyboardVisible = true;
+ }
+ scheduleUpdate();
}
};
const handleFocusOut = () => {
window.setTimeout(() => {
const active = document.activeElement;
- hasFocusedInput = Boolean(
- sheetRef.current?.contains(active) && isKeyboardInputTarget(active),
- );
- updateSheetLift();
+ activeInput =
+ sheetRef.current?.contains(active) && isKeyboardInputTarget(active)
+ ? (active as HTMLElement)
+ : null;
+ if (!activeInput) {
+ keyboardVisible = false;
+ setLift(0);
+ } else {
+ scheduleUpdate();
+ }
}, 50);
};
const unsubscribeConfig = viewPaddingsBridge.subscribeConfig((config) => {
- bridgeKbHeight = config.keyboardHeight;
- updateSheetLift();
+ const nextHeight = Math.max(0, config.keyboardHeight);
+ if (nextHeight > 0) {
+ keyboardHeight = nextHeight;
+ const active = document.activeElement as HTMLElement | null;
+ if (
+ !activeInput &&
+ active &&
+ sheetRef.current?.contains(active) &&
+ isKeyboardInputTarget(active)
+ ) {
+ activeInput = active;
+ }
+ if (activeInput) {
+ keyboardVisible = true;
+ }
+ } else {
+ keyboardHeight = 0;
+ keyboardVisible = false;
+ }
+ scheduleUpdate();
});
- const handleVpResize = () => {
- updateSheetLift();
+ const handleViewportResize = () => {
+ const vpHeight = window.visualViewport?.height ?? window.innerHeight;
+ const reduction = closedVpHeight - vpHeight;
+
+ if (reduction > KEYBOARD_THRESHOLD) {
+ if (activeInput) {
+ keyboardVisible = true;
+ keyboardHeight = reduction;
+ }
+ } else if (reduction <= KEYBOARD_THRESHOLD) {
+ keyboardVisible = false;
+ keyboardHeight = 0;
+ if (!activeInput) {
+ closedVpHeight = vpHeight;
+ }
+ }
+ scheduleUpdate();
};
document.addEventListener("focusin", handleFocusIn);
document.addEventListener("focusout", handleFocusOut);
- window.visualViewport?.addEventListener("resize", handleVpResize);
- window.visualViewport?.addEventListener("scroll", handleVpResize);
+ window.visualViewport?.addEventListener("resize", handleViewportResize);
return () => {
+ if (animFrame !== null) cancelAnimationFrame(animFrame);
document.removeEventListener("focusin", handleFocusIn);
document.removeEventListener("focusout", handleFocusOut);
- window.visualViewport?.removeEventListener("resize", handleVpResize);
- window.visualViewport?.removeEventListener("scroll", handleVpResize);
+ window.visualViewport?.removeEventListener(
+ "resize",
+ handleViewportResize,
+ );
unsubscribeConfig();
};
}, [isRendered]);
@@ -444,12 +523,11 @@ export function InformationSheet({
ref={sheetRef}
{...props}
style={{
- marginBottom: keyboardLift > 0 ? `${keyboardLift}px` : undefined,
- transition: "margin-bottom 200ms cubic-bezier(0.16, 1, 0.3, 1)",
- maxHeight:
+ transform:
keyboardLift > 0
- ? `calc(100vh - ${keyboardLift + 20}px)`
+ ? `translate3d(0, -${keyboardLift}px, 0)`
: undefined,
+ transition: "transform 200ms cubic-bezier(0.16, 1, 0.3, 1)",
...props.style,
}}
className={[
diff --git a/src/lib/view-paddings.ts b/src/lib/view-paddings.ts
index 3b6b5fc..001b72f 100644
--- a/src/lib/view-paddings.ts
+++ b/src/lib/view-paddings.ts
@@ -136,6 +136,16 @@ class ViewPaddingsBridge {
string,
any
>;
+ if (
+ envelope.action === "keyboard_changed" ||
+ data.action === "keyboard_changed"
+ ) {
+ const height = num(data.height ?? envelope.height);
+ this.config = { ...this.config, keyboardHeight: height };
+ this.applyKeyboard(height);
+ this.notifyConfigListeners();
+ return;
+ }
if (this.hasInitialConfig) {
if (data.locale) this.applyLocale(data.locale);
return;