@tarekeldeeb/quran-madina-react 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # @tarekeldeeb/quran-madina-react
2
+
3
+ A thin React component around the `<quran-madina-html>` custom element. The actual rendering is
4
+ still done by the [`quran-madina-html`](https://www.npmjs.com/package/quran-madina-html) library
5
+ (HTML/CSS, no images) — this just loads it and exposes the tag as a normal React component. The
6
+ **same** wrapper drives **web, iOS and Android** (mobile via a WebView container such as Capacitor;
7
+ see below).
8
+
9
+ The library is not bundled: the component injects its `<script>` at runtime (the library reads its
10
+ config from its own `<script>` tag, so a plain `import` of it would not work). `react` is a peer
11
+ dependency.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @tarekeldeeb/quran-madina-react react
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```jsx
22
+ import QuranMadinaHtml from "@tarekeldeeb/quran-madina-react";
23
+
24
+ export default function App() {
25
+ return (
26
+ <div dir="rtl">
27
+ {/* full page */}
28
+ <QuranMadinaHtml page={106} font="Hafs" />
29
+
30
+ {/* aya range */}
31
+ <QuranMadinaHtml sura={2} aya="8-10" />
32
+
33
+ {/* a word subsequence (1-based, inclusive) */}
34
+ <QuranMadinaHtml sura={1} aya={1} words="1:2" />
35
+
36
+ {/* no header / copy chrome */}
37
+ <QuranMadinaHtml sura={2} aya="13-14" headless />
38
+ </div>
39
+ );
40
+ }
41
+ ```
42
+
43
+ ### Props
44
+
45
+ | Prop | Type | Notes |
46
+ |-------------|---------------------|-------|
47
+ | `page` | number \| string | Full page 1–604. Mutually exclusive with `sura`/`aya` (page wins). |
48
+ | `sura` | number \| string | Sura 1–114, with `aya`. |
49
+ | `aya` | number \| string | Aya or range, e.g. `"8-10"`. |
50
+ | `words` | string | 1-based word range: `"1:2"`, `"3-10"`, or a single index. |
51
+ | `headless` | boolean | Drop the header/copy chrome. |
52
+ | `font` | string | `Hafs`, `Uthman`, `Amiri Quran`, `Amiri Quran Colored`, `me_quran`. |
53
+ | `name` | string | DB name, default `Madina05`. |
54
+ | `fontSize` | number \| string | px, default 16. |
55
+ | `src` | string | Library script URL. Default: unpkg latest. |
56
+ | `cdn` | string | Base URL for CSS + JSON assets. Default: unpkg package root. |
57
+
58
+ > **Global config caveat:** `font`, `name`, `fontSize`, `src` and `cdn` configure the single
59
+ > library instance for the whole page. Only the **first** mounted `<QuranMadinaHtml>` sets them;
60
+ > later values are ignored. Put your config on the first instance you render.
61
+
62
+ ## Why `cdn` matters (localhost gotcha)
63
+
64
+ The library figures out where to fetch its CSS + JSON DB from based on the **page** host: on any
65
+ `localhost` host it assumes the assets live at `../` (that's how this repo's own demo works). A
66
+ React dev server (`localhost:3000`) and a Capacitor app (host is also `localhost`) do **not** have
67
+ the assets at `../`, so without help the DB fetch 404s.
68
+
69
+ The wrapper fixes this by setting `data-cdn` to the unpkg package root by default, so assets always
70
+ resolve. Override `cdn` if you self-host the assets:
71
+
72
+ ```jsx
73
+ <QuranMadinaHtml page={1} cdn="https://my.cdn.example/quran-madina-html/" />
74
+ ```
75
+
76
+ > `data-cdn` support requires a build of the library that includes it (the release that introduced
77
+ > this feature, or a local `npm run build` of this repo). When loading from a CDN, pin a version
78
+ > that supports it, e.g. `src="https://unpkg.com/quran-madina-html@X.Y.Z"`.
79
+
80
+ ## Mobile: iOS + Android with Capacitor
81
+
82
+ Capacitor wraps your existing React web build in a native shell with a WebView, so the very same
83
+ component runs on device. Outline:
84
+
85
+ ```bash
86
+ # in your React app
87
+ npm install @capacitor/core @capacitor/cli
88
+ npx cap init "My Quran App" com.example.quran --web-dir=dist # or build/ for CRA
89
+ npm run build # produce the web bundle
90
+ npx cap add ios
91
+ npx cap add android
92
+ npx cap copy
93
+ npx cap open ios # opens Xcode → run on simulator/device
94
+ npx cap open android # opens Android Studio
95
+ ```
96
+
97
+ Notes for mobile:
98
+
99
+ - Capacitor serves the app from `localhost`, so keep the default `cdn` (unpkg) **or** self-host the
100
+ assets and point `cdn` at them — see above. Do not rely on the library's `../` default.
101
+ - **Offline support:** bundle the library `dist/quran-madina-html.min.js`, `dist/*.min.css`, the
102
+ `assets/db/*.json` you use and the `assets/fonts/*.woff2` into your web build, then set `src` to
103
+ the bundled JS and `cdn` to the base path that contains `dist/` and `assets/`. Everything —
104
+ including the font — resolves from `cdn`, so no network is needed (requires library >= 0.6,
105
+ which stores font paths relative).
106
+ - This is the recommended mobile path: it reuses the battle-tested HTML/CSS renderer (fonts,
107
+ per-line stretch, offsets) instead of reimplementing the layout natively in React Native.
108
+
109
+ ## React Native (native, no WebView)
110
+
111
+ Not provided here. React Native has no DOM/HTML/CSS, so the custom element cannot run; it would
112
+ require porting the renderer to native `View`/`Text` + `transform: scaleX`. Use the Capacitor path
113
+ above unless you specifically need native (non-WebView) rendering.
114
+
115
+ ## Developing this package
116
+
117
+ ```bash
118
+ cd react
119
+ npm install
120
+ npm test # vitest (jsdom) — wrapper + loader unit tests
121
+ npm run build # esbuild → dist/index.mjs + dist/index.cjs, copies dist/index.d.ts
122
+ ```
123
+
124
+ `dist/` is git-ignored and rebuilt automatically on publish (`prepublishOnly`). Source lives in
125
+ `src/`; the public API is hand-typed in `src/index.d.ts`.
126
+
127
+ ## Releasing
128
+
129
+ This package is released **in lockstep with the core library** by the repo-root command:
130
+
131
+ ```bash
132
+ npm run release # release-it: builds + publishes quran-madina-html AND this wrapper
133
+ ```
134
+
135
+ The root `release-it` config syncs this `package.json` to the core's new version before the release
136
+ commit, then publishes `@tarekeldeeb/quran-madina-react@<version>` (matching the core) after the
137
+ core lands on npm (see `scripts/sync-wrapper-version.mjs` and `scripts/publish-wrapper.mjs`). So the
138
+ wrapper version always equals the core version. A manual one-off publish is still possible with
139
+ `npm publish --access public` from this folder (scoped packages need `--access public`).
package/dist/index.cjs ADDED
@@ -0,0 +1,120 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/index.js
30
+ var index_exports = {};
31
+ __export(index_exports, {
32
+ default: () => QuranMadinaHtml,
33
+ loadQuranMadinaHtml: () => loadQuranMadinaHtml
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/QuranMadinaHtml.jsx
38
+ var import_react = __toESM(require("react"), 1);
39
+ var TAG = "quran-madina-html";
40
+ var DEFAULT_SRC = "https://unpkg.com/quran-madina-html";
41
+ var DEFAULT_CDN = "https://unpkg.com/quran-madina-html/";
42
+ var loaderPromise = null;
43
+ function loadQuranMadinaHtml(opts = {}) {
44
+ const { src = DEFAULT_SRC, cdn = DEFAULT_CDN, font, name, fontSize } = opts;
45
+ if (typeof window === "undefined" || typeof document === "undefined") {
46
+ return Promise.resolve();
47
+ }
48
+ if (loaderPromise) return loaderPromise;
49
+ if (window.customElements && window.customElements.get(TAG)) {
50
+ loaderPromise = Promise.resolve();
51
+ return loaderPromise;
52
+ }
53
+ loaderPromise = new Promise((resolve, reject) => {
54
+ const script = document.createElement("script");
55
+ script.src = src;
56
+ script.async = true;
57
+ if (cdn) script.setAttribute("data-cdn", cdn);
58
+ if (font) script.setAttribute("data-font", font);
59
+ if (name) script.setAttribute("data-name", name);
60
+ if (fontSize != null) script.setAttribute("data-font-size", String(fontSize));
61
+ script.onload = () => resolve();
62
+ script.onerror = () => {
63
+ loaderPromise = null;
64
+ reject(new Error(`Failed to load ${src}`));
65
+ };
66
+ document.head.appendChild(script);
67
+ });
68
+ return loaderPromise;
69
+ }
70
+ function QuranMadinaHtml({
71
+ page,
72
+ sura,
73
+ aya,
74
+ words,
75
+ headless,
76
+ // loader config (only honored on first mount, see loadQuranMadinaHtml)
77
+ src,
78
+ cdn,
79
+ font,
80
+ name,
81
+ fontSize,
82
+ className,
83
+ style,
84
+ ...rest
85
+ }) {
86
+ const ref = (0, import_react.useRef)(null);
87
+ const [ready, setReady] = (0, import_react.useState)(
88
+ typeof window !== "undefined" && !!window.customElements && !!window.customElements.get(TAG)
89
+ );
90
+ (0, import_react.useEffect)(() => {
91
+ let active = true;
92
+ loadQuranMadinaHtml({ src, cdn, font, name, fontSize }).then(() => active && setReady(true)).catch((err) => console.error("[quran-madina-html]", err));
93
+ return () => {
94
+ active = false;
95
+ };
96
+ }, [src, cdn, font, name, fontSize]);
97
+ (0, import_react.useEffect)(() => {
98
+ const el = ref.current;
99
+ if (!el) return;
100
+ const set = (key, value) => {
101
+ if (value === void 0 || value === null || value === false || value === "") {
102
+ el.removeAttribute(key);
103
+ } else {
104
+ el.setAttribute(key, value === true ? "" : String(value));
105
+ }
106
+ };
107
+ if (page !== void 0 && page !== null && page !== "") {
108
+ set("page", page);
109
+ set("sura", null);
110
+ set("aya", null);
111
+ } else {
112
+ set("page", null);
113
+ set("sura", sura);
114
+ set("aya", aya);
115
+ }
116
+ set("words", words);
117
+ set("headless", headless);
118
+ }, [ready, page, sura, aya, words, headless]);
119
+ return import_react.default.createElement(TAG, { ref, class: className, style, ...rest });
120
+ }
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+
3
+ export interface QuranMadinaHtmlLoaderOptions {
4
+ /** URL of the library script (default: https://unpkg.com/quran-madina-html). */
5
+ src?: string;
6
+ /** Base URL for the CSS + JSON assets (default: https://unpkg.com/quran-madina-html/). */
7
+ cdn?: string;
8
+ /** data-font: "Hafs" | "Uthman" | "Amiri Quran" | "Amiri Quran Colored" | "me_quran". */
9
+ font?: string;
10
+ /** data-name (default "Madina05"). */
11
+ name?: string;
12
+ /** data-font-size in px (default 16). */
13
+ fontSize?: number | string;
14
+ }
15
+
16
+ export interface QuranMadinaHtmlProps
17
+ extends QuranMadinaHtmlLoaderOptions,
18
+ React.HTMLAttributes<HTMLElement> {
19
+ /** Render a full Madina page (1-604). Mutually exclusive with sura/aya. */
20
+ page?: number | string;
21
+ /** Sura number (1-114). Use together with `aya`. */
22
+ sura?: number | string;
23
+ /** Aya number or range, e.g. "8-10". Use together with `sura`. */
24
+ aya?: number | string;
25
+ /** Restrict to a 1-based word range, e.g. "1:2", "3-10", or a single index. */
26
+ words?: string;
27
+ /** Drop the header / copy chrome and render only the Quran text. */
28
+ headless?: boolean;
29
+ }
30
+
31
+ /** Inject the loader script once; resolves when the custom element is registered. */
32
+ export function loadQuranMadinaHtml(
33
+ opts?: QuranMadinaHtmlLoaderOptions
34
+ ): Promise<void>;
35
+
36
+ declare const QuranMadinaHtml: React.FC<QuranMadinaHtmlProps>;
37
+ export default QuranMadinaHtml;
package/dist/index.mjs ADDED
@@ -0,0 +1,88 @@
1
+ // src/QuranMadinaHtml.jsx
2
+ import React, { useEffect, useRef, useState } from "react";
3
+ var TAG = "quran-madina-html";
4
+ var DEFAULT_SRC = "https://unpkg.com/quran-madina-html";
5
+ var DEFAULT_CDN = "https://unpkg.com/quran-madina-html/";
6
+ var loaderPromise = null;
7
+ function loadQuranMadinaHtml(opts = {}) {
8
+ const { src = DEFAULT_SRC, cdn = DEFAULT_CDN, font, name, fontSize } = opts;
9
+ if (typeof window === "undefined" || typeof document === "undefined") {
10
+ return Promise.resolve();
11
+ }
12
+ if (loaderPromise) return loaderPromise;
13
+ if (window.customElements && window.customElements.get(TAG)) {
14
+ loaderPromise = Promise.resolve();
15
+ return loaderPromise;
16
+ }
17
+ loaderPromise = new Promise((resolve, reject) => {
18
+ const script = document.createElement("script");
19
+ script.src = src;
20
+ script.async = true;
21
+ if (cdn) script.setAttribute("data-cdn", cdn);
22
+ if (font) script.setAttribute("data-font", font);
23
+ if (name) script.setAttribute("data-name", name);
24
+ if (fontSize != null) script.setAttribute("data-font-size", String(fontSize));
25
+ script.onload = () => resolve();
26
+ script.onerror = () => {
27
+ loaderPromise = null;
28
+ reject(new Error(`Failed to load ${src}`));
29
+ };
30
+ document.head.appendChild(script);
31
+ });
32
+ return loaderPromise;
33
+ }
34
+ function QuranMadinaHtml({
35
+ page,
36
+ sura,
37
+ aya,
38
+ words,
39
+ headless,
40
+ // loader config (only honored on first mount, see loadQuranMadinaHtml)
41
+ src,
42
+ cdn,
43
+ font,
44
+ name,
45
+ fontSize,
46
+ className,
47
+ style,
48
+ ...rest
49
+ }) {
50
+ const ref = useRef(null);
51
+ const [ready, setReady] = useState(
52
+ typeof window !== "undefined" && !!window.customElements && !!window.customElements.get(TAG)
53
+ );
54
+ useEffect(() => {
55
+ let active = true;
56
+ loadQuranMadinaHtml({ src, cdn, font, name, fontSize }).then(() => active && setReady(true)).catch((err) => console.error("[quran-madina-html]", err));
57
+ return () => {
58
+ active = false;
59
+ };
60
+ }, [src, cdn, font, name, fontSize]);
61
+ useEffect(() => {
62
+ const el = ref.current;
63
+ if (!el) return;
64
+ const set = (key, value) => {
65
+ if (value === void 0 || value === null || value === false || value === "") {
66
+ el.removeAttribute(key);
67
+ } else {
68
+ el.setAttribute(key, value === true ? "" : String(value));
69
+ }
70
+ };
71
+ if (page !== void 0 && page !== null && page !== "") {
72
+ set("page", page);
73
+ set("sura", null);
74
+ set("aya", null);
75
+ } else {
76
+ set("page", null);
77
+ set("sura", sura);
78
+ set("aya", aya);
79
+ }
80
+ set("words", words);
81
+ set("headless", headless);
82
+ }, [ready, page, sura, aya, words, headless]);
83
+ return React.createElement(TAG, { ref, class: className, style, ...rest });
84
+ }
85
+ export {
86
+ QuranMadinaHtml as default,
87
+ loadQuranMadinaHtml
88
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@tarekeldeeb/quran-madina-react",
3
+ "version": "0.6.1",
4
+ "description": "React wrapper for the quran-madina-html custom element — render Madina-style Quran pages (no images) in React on web and mobile (via a Capacitor WebView).",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "sideEffects": false,
21
+ "scripts": {
22
+ "build": "node build.mjs",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=16.8.0"
29
+ },
30
+ "devDependencies": {
31
+ "@testing-library/dom": "^10.4.0",
32
+ "@testing-library/react": "^16.1.0",
33
+ "esbuild": "^0.24.0",
34
+ "jsdom": "^25.0.1",
35
+ "react": "^18.3.1",
36
+ "react-dom": "^18.3.1",
37
+ "vitest": "^2.1.8"
38
+ },
39
+ "keywords": [
40
+ "quran",
41
+ "madina",
42
+ "mushaf",
43
+ "arabic",
44
+ "react",
45
+ "quran-madina-html"
46
+ ],
47
+ "author": "Tarek Eldeeb",
48
+ "license": "ISC",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/tarekeldeb/quran-madina-html.git",
52
+ "directory": "react"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/tarekeldeb/quran-madina-html/issues"
56
+ },
57
+ "homepage": "https://github.com/tarekeldeb/quran-madina-html/tree/main/react#readme"
58
+ }