إجابة مباشرة
{b.text}
من {doc.title}{doc.num ? ' — ' + (doc.instrument || '') + ' رقم ' + doc.num : ''}
{kinds.map(k => {LX_NOTE_META[k].label} )}
— لا نص —' }} />
onOpenDoc(doc.did, b.ord)}>افتح المادة في النظام
{kinds.includes('amend') &&
onOpenDoc(doc.did, b.ord)}>اعرض تعديلاتها }
{st.others && st.others.length > 0 &&
ليس هذا المقصود؟ {st.others.slice(0, 2).map(o => onOpenDoc(o.did, 0, pq.an)}>{o.title} )} }
);
}
// أفضل تطابق: عنوان النتيجة الأولى يطابق البحث ← بطاقة بارزة واختصارات
function LxBestMatch({ h, onOpenDoc }) {
const [art, setArt] = React.useState('');
const goArt = () => { const k = window.LexaRender && window.LexaRender.xrKey(art); if (k) onOpenDoc(h.did, 0, k); };
return (
أفضل تطابق
{h.sector && {h.sector} }
onOpenDoc(h.did)}>{h.title}
{h.instrument && {h.instrument}{h.num ? ' رقم ' + h.num : ''} }
{h.date_fmt && بتاريخ {h.date_fmt} }
{h.n_art > 0 && {lxNum(h.n_art)} مادة }
{h.last_amend && آخر تعديل {h.last_amend} }
onOpenDoc(h.did)}>قراءة النص
{h.n_family > 0 && onOpenDoc(h.did, 0, 0, 'family')}>اللوائح والوثائق المرتبطة ({lxNum(h.n_family)}) }
{h.n_amend_instr > 0 && onOpenDoc(h.did, 0, 0, 'timeline')}>سجل التعديلات }
{h.n_art > 0 && <>
setArt(e.target.value)} onKeyDown={e => e.key === 'Enter' && goArt()} />
اذهب
>}
);
}
function LxResultCard({ h, onOpen }) {
const [more, setMore] = React.useState(false);
const sn = h.snippets || [];
const kc = (LX_KIND_COLORS[h.kind] || {}).fg;
return (
{h.upcoming && لم يبدأ نفاذه }
{h.sector && {h.sector}{h.sub ? ' › ' + h.sub : ''} }
onOpen(h.did)} dangerouslySetInnerHTML={{ __html: h.title_html || lxEsc(h.title) }} />
{h.instrument && {h.instrument}{h.num ? ' رقم ' + h.num : ''} }
{!h.instrument && h.num && رقم {h.num} }
{h.date_fmt && بتاريخ {h.date_fmt} }
{!h.date_fmt && h.pub_hijri && نُشر في أم القرى {lxPub(h.pub_hijri)} }
{!h.is_card && h.parent_title && (
)}
{(h.n_art > 0 || h.n_amend_instr > 0 || h.n_family > 0) && (
{h.n_art > 0 && {lxNum(h.n_art)} مادة }
{h.n_amended > 0 && {lxNum(h.n_amended)} مادة معدلة }
{h.n_amend_instr > 0 && {lxNum(h.n_amend_instr)} أداة تعديل{h.last_amend ? ' · آخرها ' + h.last_amend : ''} }
{h.n_family > 0 && {lxNum(h.n_family)} وثيقة مرتبطة }
)}
{sn.length > 0 && (
{(more ? sn : sn.slice(0, 2)).map((s, i) => (
onOpen(h.did, s.ord)}>
{s.path ? — {s.path} : null}
))}
{(sn.length > 2 || h.n_unit_hits > sn.length) && (
(sn.length > 2 && !more) ? setMore(true) : onOpen(h.did, sn[0].ord)}>
{sn.length > 2 && !more ? 'عرض مطابقات أخرى' : 'تطابق في ' + lxNum(h.n_unit_hits) + ' موضعاً — افتح الوثيقة'}
)}
)}
);
}
function lxNormTitle(s) {
return String(s || '').replace(/[\u064B-\u065F\u0640]/g, '').replace(/[أإآ]/g, 'ا').replace(/ة/g, 'ه').replace(/ى/g, 'ي').replace(/\s+/g, ' ').trim();
}
function LxResults({ route, meta, go, onOpenDoc, domain = 'laws', top = null }) {
const [data, setData] = React.useState(null);
const [err, setErr] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [showF, setShowF] = React.useState(false);
const scRef = React.useRef(null);
const page = +(route.page || 1);
const qs = lxQs({
q: route.q, exact: route.exact ? 'true' : undefined, where: route.where, sector: route.sector, sub: route.sub,
kind: route.kind, status: route.status, year_from: route.yf, year_to: route.yt, scope: route.scope,
amended: route.amended ? 'true' : undefined, sort: route.sort, page: page > 1 ? page : undefined, domain,
country: route.country,
});
React.useEffect(() => {
let alive = true; setLoading(true); setErr(null);
lxGet('/api/laws/search?' + qs).then(r => { if (alive) { setData(r); setLoading(false); } })
.catch(e => { if (alive) { setErr(e.message); setLoading(false); } });
if (scRef.current) scRef.current.scrollTop = 0;
return () => { alive = false; };
}, [qs]);
const pages = data ? Math.ceil(data.total / data.size) : 0;
const act = [];
if (route.sector) act.push(['sector', route.sector + (route.sub ? ' › ' + route.sub : '')]);
if (route.kind) act.push(['kind', route.kind]);
if (route.status) act.push(['status', LX_STATUS[route.status] ? LX_STATUS[route.status].label : route.status]);
if (route.yf || route.yt) act.push(['yf', 'السنة ' + (route.yf || '…') + '–' + (route.yt || '…')]);
if (route.amended) act.push(['amended', 'عليه تعديلات']);
const drop = (k) => { const n = { ...route, page: undefined }; delete n[k]; if (k === 'sector') delete n.sub; if (k === 'yf') delete n.yt; go(n); };
const pgBtns = [];
if (pages > 1) {
const from = Math.max(1, Math.min(page - 3, pages - 6)), to = Math.min(pages, from + 6);
for (let p = from; p <= to; p++) pgBtns.push(p);
}
const direct = route.q && page === 1 && lxParseArticleQuery(route.q);
let best = null;
if (route.q && page === 1 && !direct && data && data.hits[0] && (!route.sort || route.sort === 'relevance')) {
const nt = lxNormTitle(data.hits[0].title), nq = lxNormTitle(route.q);
if (nt === nq || nt === 'نظام ' + nq || nt.replace(/\s*لعام.*$/, '') === nq) best = data.hits[0];
}
const hits = data ? data.hits.filter(h => h !== best) : [];
return (
{top}
{!top &&
go({})}>→ الرئيسية }
go({ q })} onOpenDoc={onOpenDoc} hotkey />
{loading ? 'جارٍ البحث…' : data ? (
route.q ? <>{lxNum(data.total)} نتيجة لـ «{route.q}»>
: <>{lxNum(data.total)} {route.scope === 'all' ? 'وثيقة' : 'نظاماً ولائحة رئيسية'}{route.sector ? ' في ' + route.sector : ''}>
) : ''}
{route.q && data && window.lxNbSaveSearch && window.lxNbSaveSearch(route.q, data.total)} title="احفظ هذا البحث في دفتري لترجع إلى نتائجه">🔖 احفظ في دفتري }
setShowF(v => !v)}>{showF ? 'إخفاء التصفية' : 'التصفية'}
go({ ...route, sort: e.target.value === 'relevance' ? undefined : e.target.value, page: undefined })}>
{route.q ? 'الأكثر صلة' : 'الترتيب الافتراضي'}
الأحدث إصداراً
الأقدم إصداراً
الأحدث تعديلاً
أبجدياً
{act.length > 0 &&
{act.map(([k, l]) => drop(k)}>{l} ✕ )}
}
{data && route.q &&
}
{data && route.q && !direct && page <= 1 &&
}
{direct &&
}
{err &&
{err}
}
{loading && !data && [1, 2, 3, 4].map(i =>
)}
{data && !loading && data.hits.length === 0 && (
لا توجد نتائج مطابقة جرّب كلمات أقل، أو ألغِ بعض عوامل التصفية، أو ابحث في «نص المواد».
)}
{data && !loading && route.q && !direct && data.hits.length === 0 &&
}
{best && }
{hits.map(h => )}
{data && !loading && route.q && !direct && page <= 1 && hits.length > 0 &&
h.did)} onOpenDoc={onOpenDoc} />}
{pages > 1 && (
go({ ...route, page: page - 1 })}>→
{pgBtns.map(p => go({ ...route, page: p })}>{lxNum(p)} )}
= pages} onClick={() => go({ ...route, page: page + 1 })}>←
)}
);
}
/* =========================================================
قارئ الوثيقة
========================================================= */
function LxAmendPanel({ note }) {
const p = note.pop || {};
const hasBefore = !!(p.before && !p.circular && note.kind !== 'add');
const [mode, setMode] = React.useState('cmp');
const diff = React.useMemo(() => (hasBefore && p.after && mode === 'diff') ? lxWordDiff(p.before, p.after) : null, [mode]);
const kindLbl = { amend: 'تعديل', add: 'مادة مضافة', cancel: 'إلغاء', rename: 'إحلال مسمى', note: note.label || 'ملاحظة' }[note.kind];
const kindIc = (LX_NOTE_META[note.kind] || {}).ic;
const afterTxt = String(p.after || '').replace(/\*\*/g, '');
const befLbl = note.kind === 'cancel' ? 'النص قبل الإلغاء' : 'قبل التعديل';
return (
{kindIc && {kindIc} }{kindLbl}
{(p.decree || p.date) && (
§ {p.decree ? 'بموجب ' + p.decree : ''}{p.date && {p.decree ? ' — ' : ''}وتاريخ {p.date} }
)}
{note.href &&
مستند التعديل ↗ }
{hasBefore ? (
<>
{p.after && (
setMode('cmp')}>قبل / بعد
setMode('diff')}>ما الذي تغيّر
)}
{mode === 'diff' && diff && (
حُذف أُضيف
{diff.map((x, i) => x.t === '=' ? x.s : {x.s} )}
)}
{mode === 'diff' && !diff &&
النصان طويلان جداً للمقارنة كلمة بكلمة؛ اعرض «قبل / بعد».
}
{(mode === 'cmp' || !p.after) && (
{p.after &&
بعد التعديل (النص النافذ)
{afterTxt}
}
)}
نص الحاشية كما ورد في المصدر
>
) : (
)}
);
}
function LxNoteChip({ note, open, onClick }) {
const p = note.pop || {};
const m = LX_NOTE_META[note.kind];
const base = note.kind === 'note' ? (note.label || m.label) : m.label;
// اختصار للشارة فقط؛ الصيغة الكاملة في اللوحة عند الفتح
const dec = (p.decree || '').replace(/^(?:ال)?مرسوم\s*(?:ال)?ملكي\s*(?:رقم\s*)?/, '').replace(/^قرار\s*مجلس\s*الوزراء\s*(?:رقم\s*)?/, 'قرار مجلس الوزراء ')
.replace(/^\((.*)\)$/, '$1').replace(/\s*\/\s*/g, '/');
return (
{m.ic} {base}{dec ? ' · ' + dec : ''}{p.date ? ' · ' + p.date : ''}▼
);
}
const LxArticle = React.memo(function LxArticle({ b, path, openAll, onCopy, flash, onSharh }) {
const [open, setOpen] = React.useState({});
const [explain, setExplain] = React.useState(null); // did الوثيقة حين يُطلبُ الشرحُ المبسّط
React.useEffect(() => {
if (openAll === null) return;
const o = {}; b.notes.forEach((_, i) => { o[i] = openAll; }); setOpen(o);
}, [openAll]);
const toggle = (i) => setOpen(o => ({ ...o, [i]: !o[i] }));
const secHd = b.type === 'sec' &&
;
if (b.type === 'sec' && !b.parts.length && !b.notes.length) return secHd;
if (b.type === 'sec') {
// قسم تحته نص مباشر (تمهيد الباب أو حاشية عليه): يُعرض النص بعد رأس القسم ولا يُسقط
return (
<>
{secHd}
{b.notes.length > 0 && {b.notes.map((n, i) => toggle(i)} />)}
}
{b.parts.length > 0 &&
}
{b.notes.map((n, i) => open[i] ? : null)}
>
);
}
// عنوان هو في الحقيقة فقرة كاملة: يُعرض نصاً في المتن
const body = (b.longHead ? '
' + b.headHtml + '
' : '') + b.parts.join('');
const chips = b.notes.length > 0 && (
{b.notes.map((n, i) => toggle(i)} />)}
);
return (
{path && {path}
}
{b.type === 'pre' ?
{b.text} : b.longHead ? null :
}
{chips}
{b.type !== 'pre' && (
{onSharh && 📘 الشرح الرسمي }
onCopy(b, 'text')} title="انسخ نص المادة">نسخ
onCopy(b, 'link')} title="انسخ رابطاً مباشراً لهذه المادة">رابط
window.lxNbSaveArticle && window.lxNbSaveArticle(e, b)} title="احفظ هذه المادة في دفتري">🔖 احفظ في دفتري
{ const h = e.currentTarget.closest('[data-lx-src]'); setExplain(x => x ? null : (h && h.getAttribute('data-did'))); }} title="شرحٌ مبسّط لهذه المادة بالذكاء الاصطناعي — 2 نقطة (إعادةُ فتحِه لاحقاً بلا نقاط)">✨ اشرحها ببساطة · 2 نقطة
)}
{body &&
}
{b.cancelNotice && أُلغيت هذه المادة — اضغط شارة «ملغاة» لعرض نصها قبل الإلغاء.
}
{b.notes.map((n, i) => open[i] ? : null)}
{explain && setExplain(null)} />}
);
});
function lxStateHtml(md) {
// ملاحظات الإفصاح على مستوى الوثيقة: أسطر «> …» تُعرض كما هي (غامق فقط)
return String(md || '').split('\n').map(l => l.replace(/^\s*>\s?/, '').trim()).filter(Boolean)
.map(l => '
' + (window.LexaRender ? window.LexaRender.mdInline(l) : lxEsc(l)) + '
').join('');
}
// نافذة «اذهب إلى المادة» — Ctrl+K
function LxJumpPalette({ toc, onJump, onClose }) {
const [q, setQ] = React.useState('');
const [hi, setHi] = React.useState(0);
const k = q.trim() && window.LexaRender ? window.LexaRender.xrKey(q.trim()) : 0;
const items = React.useMemo(() => {
const t = q.trim();
if (!t) return toc.filter(x => !x.sec).slice(0, 40);
const byNum = k ? toc.filter(x => x.an === String(k)) : [];
const byText = toc.filter(x => x.text.includes(t) && !byNum.includes(x));
return byNum.concat(byText).slice(0, 40);
}, [q]);
React.useEffect(() => setHi(0), [q]);
const go = (it) => { if (it) { onJump(it); onClose(); } };
return (
e.stopPropagation()}>
setQ(e.target.value)}
onKeyDown={e => {
if (e.key === 'ArrowDown') { e.preventDefault(); setHi(h => Math.min(items.length - 1, h + 1)); }
else if (e.key === 'ArrowUp') { e.preventDefault(); setHi(h => Math.max(0, h - 1)); }
else if (e.key === 'Enter') go(items[hi]);
else if (e.key === 'Escape') onClose();
}} />
{items.length === 0 &&
لا توجد مادة مطابقة.
}
{items.map((it, i) => (
setHi(i)} onClick={() => go(it)}>
{it.sec ? {it.text} : {it.text} }
{it.mk && }
{it.path && {it.path} }
))}
↑↓ تنقلEnter انتقالEsc إغلاق
);
}
function LxDocText({ doc, target, onTargetDone, viewMode, prefs, onActive, onOpenDoc, onlyReq, onCounts, sharhArts }) {
const sharhSet = React.useMemo(() => new Set(sharhArts || []), [sharhArts]);
const [openAll, setOpenAll] = React.useState(null);
const [tocOpen, setTocOpen] = React.useState(false);
const [tocQ, setTocQ] = React.useState('');
const [only, setOnly] = React.useState('all');
const [findQ, setFindQ] = React.useState('');
const [findN, setFindN] = React.useState(0);
const [findI, setFindI] = React.useState(0);
const [activeOrd, setActiveOrd] = React.useState(0);
const [collapsed, setCollapsed] = React.useState({});
const [flashOrd, setFlashOrd] = React.useState(0);
const [pal, setPal] = React.useState(false);
const [pop, setPop] = React.useState(null);
const [resume, setResume] = React.useState(null);
const contentRef = React.useRef(null);
// دفتري: يُعادُ تلوينُ ما ظلّله المستخدمُ في هذه الوثيقة
React.useEffect(() => {
const t = setTimeout(() => { if (window.LexaNote && contentRef.current) window.LexaNote.paintRef(contentRef.current, 'law:' + doc.did); }, 350);
return () => clearTimeout(t);
}, [doc && doc.did, viewMode, only]);
const popT = React.useRef(null);
const blocks = React.useMemo(() => window.LexaRender ? lxRenderDoc(doc, viewMode) : null, [doc.did, viewMode]);
// المسار (الباب › الفصل) لكل مادة، وشجرة المحتويات
const { paths, toc, counts } = React.useMemo(() => {
const paths = {}; const toc = []; let secs = [];
const counts = { amend: 0, cancel: 0, add: 0 };
// رتبة القسم بمعناه لا بعدد علامات «#» — وترتيب الأنواع من الوثيقة نفسها: النوع الذي يظهر أولاً
// هو الأعلى. أغلب الأنظمة «باب ← فصل»، وبعض اللوائح «فصل ← باب» (الفصل الثالث يحوي الباب الأول)،
// وافتراض ترتيب ثابت كان يسقط الفصل من مسار كل مادة تحت أبوابه.
const SEC_KIND = /^\s*\(?\s*(الكتاب|الباب|الفصل|الفرع|القسم|الجزء|المبحث|المطلب)/;
const kindOrder = [];
(blocks || []).forEach(b => { if (b.type === 'sec') { const m = b.text.match(SEC_KIND); if (m && !kindOrder.includes(m[1])) kindOrder.push(m[1]); } });
const rank = (b) => { const m = b.text.match(SEC_KIND); return m ? kindOrder.indexOf(m[1]) : kindOrder.length + b.lvl; };
let lastSec = null;
(blocks || []).forEach(b => {
if (b.type === 'sec') {
const r = rank(b);
secs = secs.filter(s => s.rank < r);
secs.push({ rank: r, text: b.text });
lastSec = { key: 'o' + b.ord, ord: b.ord, text: b.text, sec: true, path: secs.slice(0, -1).map(s => s.text).join(' › ') };
toc.push(lastSec);
} else {
paths[b.ord] = secs.map(s => s.text).join(' › ');
const ks = lxBlockKinds(b);
ks.forEach(k => { if (counts[k] !== undefined) counts[k]++; });
toc.push({ key: 'o' + b.ord, ord: b.ord, text: b.tocText || b.text, sec: false, an: String(b.num || b.dataArt || ''), kinds: ks, path: paths[b.ord],
mk: ks.has('cancel') ? '#c24141' : ks.has('add') ? '#2d9b62' : ks.has('amend') ? '#c96442' : null, parentSec: lastSec });
// فهرسة داخلية: عناوين سطرية داخل الكتل الضخمة
(b.anchors || []).forEach(a => toc.push({ key: a.id, ord: b.ord, elId: a.id, text: a.label, sec: false, sub: true,
an: a.num ? String(a.num) : '', kinds: new Set(), path: paths[b.ord], parentSec: lastSec }));
}
});
return { paths, toc, counts };
}, [blocks]);
// qa1: رأسُ الوثيقة يعرضُ العدّاتِ نفسَها التي في المحتويات (المتنُ المعروض) — لا عدّاتِ علاماتِ العناوين في التصدير
React.useEffect(() => { if (onCounts && blocks && viewMode === 'current') onCounts({ did: doc.did, ...counts }); }, [counts]);
const shown = React.useMemo(() => {
if (only === 'all') return blocks || [];
const keep = new Set(toc.filter(t => !t.sec && t.kinds.has(only)).map(t => t.ord));
return (blocks || []).filter(b => keep.has(b.ord));
}, [blocks, only]);
const scrollTo = (ord, flash, elId) => {
const el = document.getElementById(elId || ('lx-o-' + ord));
if (!el) return false;
el.scrollIntoView({ block: 'start' });
if (flash) { setFlashOrd(ord); setTimeout(() => setFlashOrd(0), 1800); }
return true;
};
// القفز ثم إعادة التثبيت بعد تحميل الخطوط: خط القراءة يتأخر فيطول المتن ويزيح المادة عن موضعها
const anchorTo = (ord, elId) => {
if (only !== 'all' && !shown.some(b => b.ord === ord)) setOnly('all');
setTimeout(() => {
scrollTo(ord, true, elId);
const again = () => scrollTo(ord, false, elId);
if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => setTimeout(again, 30));
setTimeout(again, 700);
}, 60);
};
const jumpItem = (t) => { setTocOpen(false); anchorTo(t.ord, t.elId); };
// شريط «N معدلة / ملغاة / مضافة» في رأس الوثيقة: يصفّي المتن على النوع المطلوب
React.useEffect(() => {
if (!onlyReq || !blocks) return;
const k = onlyReq.k;
setOnly(k && toc.some(t => !t.sec && t.kinds.has(k)) ? k : 'all');
setTimeout(() => { const el = contentRef.current && contentRef.current.closest('.lx-textwrap'); if (el) el.scrollIntoView({ block: 'start' }); }, 60);
}, [onlyReq, blocks]);
React.useEffect(() => {
if (!tocOpen) return;
const h = (e) => { if (e.key === 'Escape') setTocOpen(false); };
window.addEventListener('keydown', h);
return () => window.removeEventListener('keydown', h);
}, [tocOpen]);
// القفز إلى المادة المطلوبة (من نتيجة بحث أو إحالة أو رابط)
React.useEffect(() => {
if (!target || !blocks) return;
if (target.ord) anchorTo(target.ord);
else if (target.an) {
const hit = lxFindArt(blocks, target.an);
// احتياط: مادة مفهرسة داخل كتلة بلا عناوين
const sub = !hit && toc.find(t => t.elId && t.an === String(target.an));
if (hit) anchorTo(hit.ord); else if (sub) jumpItem(sub);
else lxToast('لم توجد المادة (' + target.an + ') في هذه الوثيقة');
}
onTargetDone && onTargetDone();
}, [target, blocks]);
// «تابع من حيث توقفت»
React.useEffect(() => {
if (target) return;
const pos = lxLoad('lx-pos', {})[doc.did];
if (pos && pos > 3) { const t = toc.find(x => x.ord === pos); if (t) setResume(t); }
}, [doc.did]);
// تتبع المادة الظاهرة + حفظ الموضع
React.useEffect(() => {
const root = contentRef.current; if (!root) return;
const sc = root.closest('.lx-rscroll');
const io = new IntersectionObserver((ents) => {
const vis = ents.filter(e => e.isIntersecting).map(e => +e.target.dataset.ord).sort((a, b) => a - b);
if (vis.length) setActiveOrd(vis[0]);
}, { root: sc, rootMargin: '-80px 0px -70% 0px' });
root.querySelectorAll('[data-ord]').forEach(el => io.observe(el));
return () => io.disconnect();
}, [shown]);
React.useEffect(() => {
if (!activeOrd) return;
const t = toc.find(x => x.ord === activeOrd);
onActive && onActive(t ? (t.sec ? t.text : t.text) : '');
const el = document.querySelector('.lx-toc-i[data-o="' + activeOrd + '"]');
if (el && el.scrollIntoViewIfNeeded) el.scrollIntoViewIfNeeded(false);
const h = setTimeout(() => { const p = lxLoad('lx-pos', {}); p[doc.did] = activeOrd; lxSave('lx-pos', p); }, 800);
return () => clearTimeout(h);
}, [activeOrd]);
// Ctrl+K
React.useEffect(() => {
const h = (e) => { if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K' || e.key === 'ك')) { e.preventDefault(); setPal(true); } };
window.addEventListener('keydown', h);
return () => window.removeEventListener('keydown', h);
}, []);
// معاينة الإحالة عند المرور: نص المادة المحال إليها دون مغادرة الموضع
const onOver = (e) => {
const a = e.target.closest && e.target.closest('a.xref');
if (!a) return;
const m = (a.getAttribute('onclick') || '').match(/xrefGo\(this,'([^']*)',(\d+)\)/);
if (!m) return;
clearTimeout(popT.current);
const r = a.getBoundingClientRect();
const pos = { top: Math.min(window.innerHeight - 360, r.bottom + 8), left: Math.max(12, Math.min(window.innerWidth - 460, r.left + r.width / 2 - 220)) };
const tgt = m[1], an = +m[2];
popT.current = setTimeout(async () => {
if (!tgt || tgt === doc.did) {
const b = lxFindArt(blocks, an);
if (b) setPop({ ...pos, title: b.text, law: '', html: b.parts.join(''), ord: b.ord });
return;
}
setPop({ ...pos, title: 'جارٍ التحميل…', html: '' });
try {
const d2 = await lxGetDoc(tgt);
const b = lxFindArt(lxRenderDoc(d2, 'current'), an);
setPop(b ? { ...pos, title: b.text, law: d2.title, html: b.parts.join(''), tgt, an } : { ...pos, title: 'لم توجد المادة في ' + d2.title, html: '' });
} catch (err) { setPop(null); }
}, 280);
};
const onOut = (e) => {
const a = e.target.closest && e.target.closest('a.xref');
if (!a) return;
clearTimeout(popT.current);
popT.current = setTimeout(() => setPop(null), 300);
};
// البحث داخل الوثيقة: إبراز في DOM دون المساس بالنص
React.useEffect(() => {
const root = contentRef.current; if (!root) return;
root.querySelectorAll('mark.lx-fm').forEach(m => { const t = document.createTextNode(m.textContent); m.replaceWith(t); });
root.normalize();
const q = findQ.trim();
if (q.length < 2) { setFindN(0); return; }
const nq = q.replace(/[\u064B-\u065F\u0670\u0640]/g, '').replace(/[أإآٱ]/g, 'ا');
const pat = new RegExp(Array.from(nq).map(ch => (ch === 'ا' ? '[اأإآٱ]' : ch === 'ه' ? '[هة]' : ch === 'ي' ? '[يى]' : ch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))).join('[\u064B-\u065F\u0670\u0640]*'), 'g');
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
const nodes = []; let n; while ((n = walker.nextNode())) { if (n.parentElement && !n.parentElement.closest('button,.lx-achips')) nodes.push(n); }
let cnt = 0;
nodes.forEach(node => {
const s = node.textContent; pat.lastIndex = 0;
if (!pat.test(s)) return; pat.lastIndex = 0;
const frag = document.createDocumentFragment(); let last = 0, m;
while ((m = pat.exec(s))) {
frag.appendChild(document.createTextNode(s.slice(last, m.index)));
const mk = document.createElement('mark'); mk.className = 'lx-fm'; mk.textContent = m[0]; frag.appendChild(mk);
last = m.index + m[0].length; cnt++;
}
frag.appendChild(document.createTextNode(s.slice(last)));
node.replaceWith(frag);
});
setFindN(cnt); setFindI(0);
}, [findQ, shown]);
React.useEffect(() => {
const root = contentRef.current; if (!root || !findN) return;
const ms = root.querySelectorAll('mark.lx-fm');
ms.forEach(m => m.classList.remove('cur'));
const m = ms[findI]; if (m) { m.classList.add('cur'); m.scrollIntoView({ block: 'center' }); }
}, [findI, findN]);
const onCopy = (b, what) => {
if (what === 'link') {
lxCopy(location.origin + location.pathname + '?' + lxQs({ screen: 'search', doc: doc.did, art: b.ord }), 'نُسخ رابط المادة');
} else {
const body = (document.getElementById('lx-o-' + b.ord)?.querySelector('.lx-art-b')?.innerText || '').trim();
lxCopy(b.text + '\n' + body + '\n— ' + doc.title, 'نُسخ نص المادة');
}
};
if (!window.LexaRender) return
تعذّر عرض نص الوثيقة الآن. أعد تحميل الصفحة بعد قليل.
;
if (!doc.text) return
لا يتوفر نص لهذه الوثيقة في المصادر المتاحة لنا بياناتها الوصفية وعلاقاتها متاحة في التبويبات الأخرى.
;
const tq = tocQ.trim();
const tocShown = toc.filter(t => {
if (only !== 'all') return !t.sec && t.kinds.has(only);
if (tq) return t.text.includes(tq);
if (!t.sec && t.parentSec && collapsed[t.parentSec.ord]) return false;
return true;
});
const hasSecs = toc.some(t => t.sec);
return (
setTocOpen(false)} />
المحتويات setTocQ(e.target.value)} /> setTocOpen(false)} aria-label="إغلاق المحتويات">✕
{(counts.amend + counts.cancel + counts.add) > 0 && viewMode !== 'before' && (
setOnly('all')}>الكل
{['amend', 'cancel', 'add'].filter(k => counts[k]).map(k => (
setOnly(only === k ? 'all' : k)}>
{LX_NOTE_META[k].label} {lxNum(counts[k])}
))}
)}
{tocShown.map(t => (
jumpItem(t)}>
{t.sec && hasSecs && !tq && only === 'all' && { e.stopPropagation(); setCollapsed(c => ({ ...c, [t.ord]: !c[t.ord] })); }}>{collapsed[t.ord] ? '◂' : '▾'} }
{t.text}
{t.mk && }
))}
setTocOpen(true)} aria-label="المحتويات">☰ المحتويات{only !== 'all' ? ' •' : ''}
setFindQ(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && findN) setFindI(i => e.shiftKey ? (i - 1 + findN) % findN : (i + 1) % findN); }} />
{findQ.trim().length >= 2 &&
{findN ? lxNum(findI + 1) + ' من ' + lxNum(findN) : 'لا نتائج'} }
{findN > 1 && <>
setFindI(i => (i - 1 + findN) % findN)}>▲
setFindI(i => (i + 1) % findN)}>▼
>}
setPal(true)} title="اذهب إلى مادة برقمها" aria-label="اذهب إلى مادة"># اذهب إلى مادة Ctrl K
{viewMode !== 'before' && counts.amend > 0 && (
setOpenAll(o => !o)}>{openAll ? 'طي التعديلات' : 'فتح كل التعديلات'}
)}
{resume && (
توقفت في المرة السابقة عند «{resume.text}»
{ anchorTo(resume.ord); setResume(null); }}>تابع القراءة
setResume(null)}>✕
)}
{viewMode === 'before' && (
تعرض هذه النسخة النص قبل التعديلات، مُعاداً بناؤه من النصوص السابقة المقتبسة في حواشي التعديل. المادة التي لم يُنقل نصها السابق تظهر بنصها الحالي. للنص النافذ اختر «النص النافذ».
)}
{only !== 'all' && (
تعرض المواد {LX_NOTE_META[only].label === 'معدلة' ? 'المعدلة' : LX_NOTE_META[only].label === 'ملغاة' ? 'الملغاة' : 'المضافة'} فقط ({lxNum(shown.length)}). setOnly('all')}>اعرض النص كاملاً
)}
{shown.map(b => window.lxOpenArticle({ kind: 'sharh', did: doc.did, an: +(b.dataArt || b.num) }) : null} />)}
{pal &&
setPal(false)} />}
{pop && (
clearTimeout(popT.current)} onMouseLeave={() => setPop(null)}>
)}
);
}
function LxDocInfo({ doc }) {
const urls = doc.urls || {};
const uqn = doc.uqn || [];
const scope = doc.scope || {};
const srcName = { boe: 'هيئة الخبراء بمجلس الوزراء', ncar: 'المركز الوطني للوثائق والمحفوظات', uqn: 'جريدة أم القرى', moj: 'وزارة العدل' };
const rows = [
['نوع الوثيقة', doc.kind],
['أداة الإصدار', doc.instrument],
['رقم الأداة', doc.num],
['تاريخ الإصدار', (doc.date_fmt || doc.date) ? (doc.date_fmt || doc.date) + (LX_DATE_SRC[doc.date_src] ? ' — ' + LX_DATE_SRC[doc.date_src] : '') : ''],
['النشر في أم القرى', doc.pub_hijri ? lxPub(doc.pub_hijri) + (doc.pub_greg ? ' (' + doc.pub_greg + 'م)' : '') : ''],
['الحالة', LX_STATUS[doc.status] ? LX_STATUS[doc.status].label : doc.status],
['آخر تعديل على الوثيقة', doc.last_amend ? doc.last_amend + (doc.n_amend_instr ? ' (' + lxNum(doc.n_amend_instr) + ' أداة تعديل)' : '') : ''],
['آخر تعديل في العائلة', doc.fam_last_amend && doc.fam_last_amend !== doc.last_amend ? doc.fam_last_amend + ' — على لائحة أو وثيقة تابعة' : ''],
['المجال', doc.sector ? doc.sector + (doc.sub ? ' › ' + doc.sub : '') : ''],
['عدد المواد', doc.n_art ? lxNum(doc.n_art) + (doc.n_amended ? ' — منها ' + lxNum(doc.n_amended) + ' معدلة' : '') + (doc.n_cancelled ? ' و' + lxNum(doc.n_cancelled) + ' ملغاة' : '') + (doc.n_added ? ' و' + lxNum(doc.n_added) + ' مضافة' : '') : ''],
].filter(r => r[1]);
return (
{rows.map(([k, v]) =>
)}
{uqn[0] &&
جريدة أم القرى العدد {uqn[0]}{uqn[1] ? ' بتاريخ ' + uqn[1] : ''}{uqn[2] ? <> —
عرض في الجريدة ↗ > : null}
}
المصادر الرسمية
{Object.entries(urls).map(([k, u]) =>
)}
{doc.pdf &&
}
{!Object.keys(urls).length && !doc.pdf &&
— لا توجد روابط مصدر مسجلة لهذه الوثيقة.
}
{(doc.nafad || Object.keys(scope).length > 0) && (
{doc.nafad &&
}
{Object.entries(scope).map(([k, v]) =>
{k} {typeof v === 'string' ? v : JSON.stringify(v)}
)}
)}
);
}
function LxFamily({ doc, onOpen }) {
const fam = doc.family;
const [q, setQ] = React.useState('');
const [more, setMore] = React.useState({});
if (!fam) return
لا توجد وثائق مرتبطة مسجلة
;
const nq = lxNormTitle(q);
const match = (it) => !nq || lxNormTitle(it.title).includes(nq) || (it.nested || []).some(s => lxNormTitle(s.title).includes(nq));
const total = Object.values(fam.buckets || {}).reduce((a, b) => a + b.length, 0);
return (
{!doc.is_card && doc.parent && (
)}
{total > 8 && (
setQ(e.target.value)} />
)}
{LX_BUCKETS.map(([k, label, desc]) => {
const items = ((fam.buckets || {})[k] || []).filter(match);
if (!items.length) return null;
const sorted = items.slice().sort((a, b) => (b.hkey || 0) - (a.hkey || 0));
const lim = more[k] || nq ? sorted.length : 8;
return (
{label} {desc} {lxNum(items.length)}
{sorted.slice(0, lim).map(it => (
onOpen(it.did)}>
{it.title}
{it.status !== 'ساري' && }
{it.date || 'بلا تاريخ'}{!it.has_text ? ' · بلا نص' : ''}
{it.nested && it.nested.length > 0 && (
{it.nested.map(s => (
onOpen(s.did)}>
✎
{s.title}
{s.date || 'بلا تاريخ'}
))}
)}
))}
{sorted.length > lim &&
setMore(m => ({ ...m, [k]: true }))}>عرض الكل ({lxNum(sorted.length)}) }
);
})}
);
}
function LxTimeline({ doc, onOpen }) {
const tl = doc.timeline || {};
const hasNested = [...(tl.dated || []), ...(tl.undated || [])].some(r => r.type === 'nested');
const [scope, setScope] = React.useState('own');
const keep = r => scope === 'all' || r.type !== 'nested';
const rows = (tl.dated || []).filter(keep), und = (tl.undated || []).filter(keep);
const head = hasNested && (
setScope('own')}>تعديلات هذه الوثيقة
setScope('all')}>ومعها تعديلات لوائحها وتوابعها
);
if (!rows.length && !und.length) return
{head}
لا توجد أدوات تعديل مسجلة على هذه الوثيقة
;
let lastY = null;
return (
{head}
{(tl.origin || {}).date || 'بلا تاريخ مسجل'}
الإصدار — {doc.title}
{(tl.origin || {}).instrument}{(tl.origin || {}).num ? ' رقم ' + tl.origin.num : ''}
{rows.map(r => {
const y = Math.floor((r.hkey || 0) / 10000);
const showY = y && y !== lastY; lastY = y;
return (
{showY && {y}هـ
}
onOpen(r.did)}>
{r.date}{r.num ? ' · رقم ' + r.num : ''}
{r.title}
{r.on &&
على: {r.on}
}
);
})}
{und.length > 0 && (
<>
{lxNum(und.length)} أداة بلا تاريخ مسجل — وُضعت في آخر القائمة ولم تُرتب في موضع تخميني.
{und.map(r => (
onOpen(r.did)}>
بلا تاريخ{r.num ? ' · رقم ' + r.num : ''}
{r.title}
{r.on &&
على: {r.on}
}
))}
>
)}
);
}
// النص الإنجليزي: عناوين وفقرات، دون تغيير في الكلمات
function LxEnglish({ text }) {
const html = String(text || '').split('\n').map(l => l.trim()).filter(Boolean).map(l => {
const m = l.match(/^#{1,6}\s+(.*)$/);
const k = l.match(/^_\[(.+)\]_$/); // en1: علامةُ مصدرِ المادة (ترجمة ليكسا / رسمية) أو تنويهُ الوثيقة
if (k) return '
' + lxEsc(k[1]) + '
';
return m ? '
' + lxEsc(m[1]) + ' ' : '
' + lxEsc(l).replace(/\*\*(.+?)\*\*/g, '$1 ') + '
';
}).join('');
return
;
}
function LxPrefs({ prefs, setPrefs }) {
const [open, setOpen] = React.useState(false);
const set = (p) => { const n = { ...prefs, ...p }; setPrefs(n); lxSave('lx-prefs', n); };
const boxRef = React.useRef(null);
React.useEffect(() => {
if (!open) return;
const h = (e) => { if (boxRef.current && !boxRef.current.contains(e.target)) setOpen(false); };
const k = (e) => { if (e.key === 'Escape') setOpen(false); };
document.addEventListener('pointerdown', h); window.addEventListener('keydown', k);
return () => { document.removeEventListener('pointerdown', h); window.removeEventListener('keydown', k); };
}, [open]);
return (
setOpen(o => !o)} title="إعدادات القراءة">Aa
{open && (
حجم الخط
{[[15, 'صغير'], [17, 'متوسط'], [19, 'كبير'], [21, 'أكبر']].map(([v, l]) => set({ fs: v })}>{l} )}
لون الصفحة
{[['paper', 'الأساسي'], ['white', 'أفتح'], ['night', 'ليلي']] /* au1: الألوانُ الثلاثةُ داكنة — «فاتح/ورقي» كانا مضلِّلين */.map(([v, l]) => set({ theme: v })}>{l} )}
set({ focus: e.target.checked })} />وضع التركيز (إخفاء الفهرس)
)}
);
}
function LxDocReader({ route, go, onOpenDoc, onBack, prefs, setPrefs }) {
const did = route.doc;
const [doc, setDoc] = React.useState(null);
const [err, setErr] = React.useState(null);
const [tab, setTab] = React.useState(route.tab || 'text');
const [viewMode, setViewMode] = React.useState('current');
const [en, setEn] = React.useState(null);
const [target, setTarget] = React.useState(route.art || route.an ? { ord: +route.art || 0, an: +route.an || 0 } : null);
const [prog, setProg] = React.useState(0);
const [compact, setCompact] = React.useState(false);
const [activeLbl, setActiveLbl] = React.useState('');
const [liveN, setLiveN] = React.useState(null); // qa1: عدّاتُ المعدّلة/الملغاة/المضافة من المتن المعروض
const [gd, setGd] = React.useState(null); // guides1: الشروحُ والأدلّةُ الرسميّة + الموادُّ التي لها شرحٌ رسميّ
const scRef = React.useRef(null);
React.useEffect(() => {
let alive = true; setGd(null);
lxGet('/api/laws/guides/' + encodeURIComponent(did)).then(r => alive && setGd(r)).catch(() => {});
return () => { alive = false; };
}, [did]);
React.useEffect(() => {
let alive = true; setDoc(null); setErr(null); setEn(null); setViewMode('current'); setCompact(false); setProg(0);
lxGetDoc(did).then(d => { if (alive) { setDoc(d); lxRememberDoc(d); } }).catch(e => alive && setErr(e.message));
if (scRef.current) scRef.current.scrollTop = 0;
return () => { alive = false; };
}, [did]);
React.useEffect(() => { setTab(route.tab || 'text'); }, [did, route.tab]);
React.useEffect(() => { if (route.art || route.an) { setTarget({ ord: +route.art || 0, an: +route.an || 0 }); setTab('text'); } }, [route.art, route.an, did]);
// الإحالات داخل المتن (xrefGo في الشفرة المنسوخة)
React.useEffect(() => {
const h = (e) => {
const { tgt, art } = e.detail || {};
if (!tgt || tgt === did) setTarget({ ord: 0, an: art });
else onOpenDoc(tgt, 0, art);
};
window.addEventListener('lx:xref', h);
return () => window.removeEventListener('lx:xref', h);
}, [did]);
const onScroll = (e) => {
const el = e.currentTarget;
const p = el.scrollHeight > el.clientHeight ? el.scrollTop / (el.scrollHeight - el.clientHeight) : 0;
setProg(p);
setCompact(el.scrollTop > 240);
};
const toggleEn = () => {
if (viewMode === 'en') { setViewMode('current'); return; }
if (en) { setViewMode('en'); return; }
lxGet('/api/laws/doc/' + encodeURIComponent(did) + '/en').then(r => { setEn(r.text); setViewMode('en'); setTab('text'); }).catch(() => lxToast('لا توجد ترجمة إنجليزية'));
};
const setTabR = (t) => { setTab(t); go({ doc: did, tab: t === 'text' ? undefined : t }, true); };
const [onlyReq, setOnlyReq] = React.useState(null);
const focusText = (only) => { if (viewMode !== 'current') setViewMode('current'); setTabR('text'); setOnlyReq({ k: only || 'all', n: Date.now() }); };
if (err) return
;
if (!doc) return
;
const fam = doc.family;
const famCount = fam ? Object.values(fam.buckets || {}).reduce((a, b) => a + b.length, 0) : 0;
const tl = doc.timeline || {};
const tlCount = [...(tl.dated || []), ...(tl.undated || [])].filter(r => r.type !== 'nested').length;
const upc = doc.upcoming;
const upLeft = upc ? lxDaysLeft(upc) : null;
const execN = fam && doc.is_card ? ((fam.buckets || {}).executive_regs || []).length : 0;
const ln = liveN && liveN.did === doc.did ? liveN : null; // qa1
const nAm = ln ? ln.amend : doc.n_amended, nCa = ln ? ln.cancel : doc.n_cancelled, nAd = ln ? ln.add : doc.n_added;
return (
→ رجوع
{compact ? (
{doc.title} {tab === 'text' && activeLbl && › {activeLbl} }
) : (
)}
{tab === 'text' &&
}
{doc.has_en &&
English }
{
// au1: قائمةُ المشاركة في الجوّال (navigator.share)، والنسخُ في غيرِه
const url = location.origin + location.pathname + '?' + lxQs({ screen: 'search', doc: did });
if (navigator.share && window.matchMedia && window.matchMedia('(pointer:coarse)').matches) {
navigator.share({ title: (doc && doc.title) || 'ليكسا', url }).catch(() => {});
} else lxCopy(url, 'نُسخ رابط الوثيقة');
}}>مشاركة
window.print()}>طباعة
{(doc.sources || []).length > 0 && المصادر: {doc.sources.join(' · ')} }
window.lxNbSaveDoc && window.lxNbSaveDoc(doc)} title="احفظ هذا النظام في دفتري">🔖 احفظ في دفتري
{doc.title}
{doc.instrument &&
أداة الإصدار {doc.instrument}{doc.num ? ' رقم ' + doc.num : ''}
}
{!doc.instrument && doc.num &&
الرقم {doc.num}
}
{(doc.date_fmt || doc.date) &&
الإصدار {doc.date_fmt || doc.date}
}
{doc.pub_hijri &&
النشر في أم القرى {lxPub(doc.pub_hijri)}
}
{doc.last_amend &&
آخر تعديل {doc.last_amend}
}
{doc.execreg_parent_doc &&
}
{!doc.is_card && doc.parent &&
}
{upc && (upLeft == null || upLeft >= 0) && (
{upLeft != null && upLeft >= 0 &&
}
نُشر هذا النظام في جريدة أم القرى بتاريخ {String(upc.published_hijri || '').replace(/-/g, '/')}هـ ولم يبدأ نفاذه بعد{lxEffDate(upc) ? '؛ يبدأ العمل به في ' + lxEffDate(upc) : ''}.
{upLeft != null && upLeft >= 0 && (upc.observed || {}).hijri ? ' المدة المتبقية محسوبة من رصد الجريدة في ' + upc.observed.hijri + 'هـ.' : ''}
{upc.url && <>
الصفحة في الجريدة ↗ >}
)}
{(doc.n_art > 0 || execN > 0 || tlCount > 0) && (
{doc.n_art > 0 && focusText('all')}>{lxNum(doc.n_art)} مادة }
{nAm > 0 && focusText('amend')} title="اعرض المواد المعدلة فقط">{lxNum(nAm)} معدلة }
{nCa > 0 && focusText('cancel')} title="اعرض المواد الملغاة فقط">{lxNum(nCa)} ملغاة }
{nAd > 0 && focusText('add')} title="اعرض المواد المضافة فقط">{lxNum(nAd)} مضافة }
{execN > 0 && setTabR('family')}>{lxNum(execN)} لائحة تنفيذية }
{tlCount > 0 && setTabR('timeline')}>{lxNum(tlCount)} أداة تعديل }
)}
setTabR('text')}>النص
setTabR('family')}>الوثائق المرتبطة{famCount ? {lxNum(famCount)} : null}
setTabR('timeline')}>سجل التعديلات{tlCount ? {lxNum(tlCount)} : null}
setTabR('info')}>بيانات الوثيقة
{gd && gd.guides.length > 0 &&
setTabR('guides')}>📘 الشروح والأدلة الرسمية{lxNum(gd.guides.length)} }
{tab === 'text' && doc.n_amended > 0 && viewMode !== 'en' && (
setViewMode('current')}>النص النافذ
setViewMode('before')}>قبل التعديلات
)}
{tab === 'text' && viewMode === 'en' && }
{tab === 'text' && viewMode !== 'en' && setTarget(null)} viewMode={viewMode} prefs={prefs} onActive={setActiveLbl} onOpenDoc={onOpenDoc} onlyReq={onlyReq} onCounts={setLiveN} sharhArts={gd && gd.sharh_arts} />}
{tab === 'guides' && gd && }
{tab === 'info' && }
{tab === 'family' && }
{tab === 'timeline' && }
);
}
/* =========================================================
الجذر
========================================================= */
function LxToast() {
const [msg, setMsg] = React.useState('');
React.useEffect(() => {
let t;
const h = (e) => { setMsg(e.detail); clearTimeout(t); t = setTimeout(() => setMsg(''), 1900); };
window.addEventListener('lx:toast', h);
return () => window.removeEventListener('lx:toast', h);
}, []);
if (!msg) return null;
return
{msg}
;
}
/* =========================================================
المعاهدات والاتفاقيات الدولية — قسمٌ مستقلٌّ (domain=treaties)
========================================================= */
function LxTreaties({ route, meta, go, onOpenDoc, onSection }) {
const tgo = (r, replace) => go({ sec: 'treaties', ...r }, replace);
const ts = meta && (meta.sectors || []).find(s => s.name === LX_TREATY_SECTOR);
const subs = ts ? ts.subs.filter(x => x.docs).sort((a, b) => b.docs - a.docs) : [];
const r = { ...route, scope: 'all', sort: route.sort || (route.q ? undefined : 'newest') };
const pick = (sub) => tgo({ q: route.q, country: route.country, sub: sub || undefined, sector: sub ? LX_TREATY_SECTOR : undefined });
// الدولةُ الطرف — قائمةٌ بعدّاتها (تتبع التصنيفَ المختار)
const [countries, setCountries] = React.useState([]);
const [cq, setCq] = React.useState('');
const [open, setOpen] = React.useState(false);
React.useEffect(() => {
let alive = true;
lxGet('/api/laws/treaty-countries' + (route.sub ? '?sub=' + encodeURIComponent(route.sub) : ''))
.then(x => { if (alive) setCountries(x.items || []); }).catch(() => {});
return () => { alive = false; };
}, [route.sub]);
const pickCountry = (c) => { setOpen(false); setCq(''); tgo({ q: route.q, sub: route.sub, sector: route.sector, country: c || undefined }); };
const nz = (t) => String(t || '').replace(/[أإآ]/g, 'ا').replace(/ة/g, 'ه').replace(/ى/g, 'ي').replace(/[ُ]/g, '');
const BLOCS = ['مجلس التعاون الخليجي', 'جامعة الدول العربية', 'منظمة التعاون الإسلامي', 'الأمم المتحدة ووكالاتها', 'متعددة الأطراف (دولية)'];
const shown = countries.filter(c => !cq || nz(c.name).includes(nz(cq)));
const quick = countries.filter(c => !BLOCS.includes(c.name)).slice(0, 10);
const top = (
🌍 المعاهدات والاتفاقيات الدولية
الاتفاقيات الثنائية والخليجية والعربية والإسلامية والأممية التي انضمت إليها المملكة، وقرارات الموافقة عليها{ts ? ' — ' + lxNum(ts.docs) + ' وثيقة' : ''}
🏳️ الدولة / الجهة الطرف:
setOpen(o => !o)}>
{route.country || 'كل الدول'} ▾
{open && (
setOpen(false)}>
setCq(e.target.value)} />
pickCountry(null)}>كل الدول
{shown.map(c => (
pickCountry(c.name)}>
{c.name} {lxNum(c.n)}
))}
{!shown.length &&
لا توجد دولة بهذا الاسم
}
)}
{!route.country && quick.map(c => (
pickCountry(c.name)}>{c.name} ({lxNum(c.n)})
))}
{route.country &&
pickCountry(null)}>✕ إلغاء }
{subs.length > 0 && (
pick()}>الكل
{subs.map(x => (
pick(route.sub === x.name ? null : x.name)}>
{x.name} ({lxNum(x.docs)})
))}
)}
);
return
;
}
/* ── sheet1: لوحةُ المادّة — إحالةُ المساعد/الخلاصة تفتحُ المادّةَ هنا، لا تبويبٌ جديدٌ ولا إعادةُ تحميلٍ للمنصّة.
تجلبُ المادّةَ وحدَها (/api/laws/article — بضعةُ كيلوبايتات بدلَ الوثيقة كلِّها)، وزرُّ «رجوع» الجهازِ يُغلقُها،
و«الوثيقة كاملة» تنتقلُ داخلَ المنصّة ويُرجعُ «رجوع» إلى الشاشةِ السابقة. ── */
const LX_SHEET_CSS = `
.lxas-bd{position:fixed;inset:0;z-index:9000;background:rgba(5,9,8,.62);display:flex;align-items:flex-end;justify-content:center;animation:lxasFade .16s ease both}
.lxas{position:relative;width:min(760px,100%);height:min(88vh,860px);background:#16211d;border-radius:18px 18px 0 0;display:flex;flex-direction:column;overflow:hidden;
box-shadow:0 -10px 40px rgba(0,0,0,.45);animation:lxasUp .22s cubic-bezier(.2,.8,.2,1) both;direction:rtl}
@media (min-width:769px){.lxas-bd{align-items:center}.lxas{border-radius:18px;height:min(82vh,820px)}}
.lxas-grip{width:38px;height:4px;border-radius:4px;background:rgba(239,231,214,.18);margin:8px auto 0;flex-shrink:0}
.lxas-h{display:flex;align-items:flex-start;gap:10px;padding:10px 16px 10px;border-bottom:1px solid rgba(239,231,214,.08);flex-shrink:0}
.lxas-h .t{flex:1;min-width:0}
.lxas-h .doc{font-size:12.5px;color:#a8a69e;line-height:1.6;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
.lxas-h .par{font-size:11.5px;color:#8f9a94}
.lxas-x{border:0;background:rgba(239,231,214,.07);color:#efe7d6;width:36px;height:36px;border-radius:10px;font-size:18px;cursor:pointer;flex-shrink:0}
.lxas-b{flex:1;overflow:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;padding:6px 14px 18px}
.lxas-b .lx-root{height:auto}
.lxas-f{display:flex;gap:8px;padding:10px 14px calc(10px + env(safe-area-inset-bottom));border-top:1px solid rgba(239,231,214,.08);flex-shrink:0;background:#16211d}
.lxas-f button{border:0;border-radius:11px;padding:10px 12px;font:inherit;font-size:13.5px;cursor:pointer;background:rgba(239,231,214,.07);color:#efe7d6}
.lxas-f button:disabled{opacity:.35;cursor:default}
.lxas-f .full{margin-inline-start:auto;background:#c96442;color:#fff;font-weight:600}
.lxas-sk{height:14px;border-radius:7px;margin:14px 0;background:linear-gradient(90deg,rgba(239,231,214,.05),rgba(239,231,214,.11),rgba(239,231,214,.05));background-size:200% 100%;animation:lxsk 1.2s linear infinite}
.lxas-msg{margin:28px 6px;color:#c9c2b3;font-size:14px;text-align:center}
.lxas-badge{display:inline-block;font-size:11.5px;font-weight:700;color:#9cc3e8;background:rgba(143,184,224,.12);border-radius:20px;padding:1px 9px;margin-bottom:3px}
.lxas-f .sharh{background:rgba(143,184,224,.14);color:#9cc3e8;font-weight:600}
.lxas-src{margin-inline-start:auto;align-self:center;color:#ffb08f;font-size:13.5px;text-decoration:none}
.lxas-lib{padding:8px 4px;font-family:'Noto Naskh Arabic','IBM Plex Sans Arabic',serif;font-size:16.5px;line-height:2}
.lx-guides{max-width:860px;margin:0 auto;padding:6px 0 30px}
.lx-guides-note{color:#a8a69e;font-size:13.5px;margin:4px 2px 14px}
.lx-guide{display:flex;align-items:center;gap:12px;padding:12px 14px;margin-bottom:8px;border-radius:14px;background:#16211d;box-shadow:0 0 0 1px rgba(239,231,214,.08)}
.lx-guide .t{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.lx-guide .t b{font-size:14.5px;color:#efe7d6;line-height:1.6}
.lx-guide .t span{font-size:12.5px;color:#8f9a94}
.lx-guide .a{display:flex;align-items:center;gap:10px;flex-shrink:0}
.lx-guide .a button{border:0;border-radius:10px;padding:8px 14px;font:inherit;font-size:13px;font-weight:600;cursor:pointer;background:rgba(143,184,224,.14);color:#9cc3e8}
.lx-guide .a a{color:#ffb08f;font-size:12.5px;text-decoration:none}
@media (max-width:768px){.lx-guide{flex-direction:column;align-items:stretch}.lx-guide .a{justify-content:space-between}}
.lx-root .lx-act-sharh{background:rgba(143,184,224,.14)!important;color:#9cc3e8!important}
.lxas-toast{position:absolute;bottom:72px;left:50%;transform:translateX(-50%);background:#efe7d6;color:#0f1715;border-radius:20px;padding:6px 14px;font-size:13px;font-weight:600}
@keyframes lxasFade{from{opacity:0}}
@keyframes lxasUp{from{transform:translateY(40px);opacity:.4}}
@media (prefers-reduced-motion:reduce){.lxas-bd,.lxas{animation:none}}
`;
const lxArticleCache = new Map();
function lxGetArticle(did, art, an) {
const k = did + '|' + (art || '') + '|' + (an || '');
if (!lxArticleCache.has(k)) {
const p = lxGet('/api/laws/article/' + encodeURIComponent(did) + '?' + lxQs({ art: art || undefined, an: an || undefined }));
p.catch(() => lxArticleCache.delete(k));
lxArticleCache.set(k, p);
if (lxArticleCache.size > 40) lxArticleCache.delete(lxArticleCache.keys().next().value);
}
return lxArticleCache.get(k);
}
// «الوثيقة كاملة»: انتقالٌ داخلَ المنصّة — والمدخلُ الحاليُّ في السجلّ يحفظُ الشاشةَ السابقة ليعيدَها «رجوع»
function lxOpenFullDoc(did, ord) {
try { history.replaceState({ ...(history.state || {}), lxSheet: 0, lxScreen: window.__lxScreen || 'assistant' }, ''); } catch (_) {}
history.pushState({ lxScreen: 'search' }, '', location.pathname + '?' + lxQs({ screen: 'search', doc: did, art: ord }));
window.dispatchEvent(new PopStateEvent('popstate', { state: { lxScreen: 'search' } }));
}
// guides1: اللوحةُ نفسُها بثلاثةِ أوضاع — مادّة · شرحُها الرسميّ (kind:'sharh') · دليلٌ رسميٌّ كامل (kind:'guide')
function lxSheetLoad(cur) {
if (cur.kind === 'sharh') return lxGet('/api/laws/sharh/' + encodeURIComponent(cur.did) + '?' + lxQs({ an: cur.an }));
if (cur.kind === 'guide') return lxGet('/api/laws/guide?' + lxQs({ id: cur.id }));
return lxGetArticle(cur.did, cur.art, cur.an);
}
function LxArticleSheet({ req, onClose }) {
const [cur, setCur] = React.useState(req);
const [st, setSt] = React.useState({ loading: true });
const [toast, setToast] = React.useState('');
const bodyRef = React.useRef(null);
const kind = cur.kind || 'article';
React.useEffect(() => {
let alive = true;
setSt(s => ({ ...s, loading: true, err: null }));
lxSheetLoad(cur).then(d => {
if (!alive) return;
let b = null;
if (kind === 'article') {
window.AMEND_LINKS = window.AMEND_LINKS || {};
const blocks = window.LexaRender ? lxRenderDoc({ did: d.did, text: d.md, amend_links: d.amend_links }, 'current') : [];
b = blocks.find(x => x.type === 'art') || blocks.find(x => x.parts.length || x.notes.length) || null;
}
setSt({ d, b, kind });
if (bodyRef.current) bodyRef.current.scrollTop = 0;
}).catch(e => alive && setSt({ err: (e && e.message) || 'تعذّر الفتح', kind }));
return () => { alive = false; };
}, [cur.kind, cur.did, cur.art, cur.an, cur.id]);
React.useEffect(() => {
const k = (e) => { if (e.key === 'Escape') onClose(); };
const t = (e) => { setToast(e.detail || ''); setTimeout(() => setToast(''), 1600); };
window.addEventListener('keydown', k);
window.addEventListener('lx:toast', t);
const ov = document.body.style.overflow; document.body.style.overflow = 'hidden';
return () => { window.removeEventListener('keydown', k); window.removeEventListener('lx:toast', t); document.body.style.overflow = ov; };
}, []);
const d = st.kind === kind ? st.d : null;
const step = (k) => d && (kind === 'sharh' ? setCur({ kind: 'sharh', did: cur.did, an: d.an + k, back: cur.back }) : setCur({ did: d.did, art: d.ord + k }));
const onCopy = (b, what) => {
if (!d) return;
if (what === 'link') lxCopy(location.origin + location.pathname + '?' + lxQs({ screen: 'search', doc: d.did, art: d.ord }), 'نُسخ رابط المادة');
else lxCopy((d.heading || '') + '\n' + ((bodyRef.current && bodyRef.current.querySelector('.lx-art-b') || {}).innerText || '').trim() + '\n— ' + d.title, 'نُسخ نص المادة');
};
const head = kind === 'article'
? <>
{d ? d.title : (cur.label || '…')}
{d && d.parent &&
ضمن: {d.parent.title}
}>
: <>
📘 {kind === 'sharh' ? 'الشرح الرسمي' : 'دليلٌ رسمي'}
{d ? d.title : (cur.label || '…')}
{d && d.issuer &&
{d.issuer}{kind === 'sharh' ? ' — المادة ' + d.an : ''}
}>;
let body;
if (st.err) body =
{st.err}
;
else if (!d) body =
;
else if (kind === 'article') body = !st.b ?
لا يتوفّر نصٌّ لهذه المادة.
:
;
else body =
;
return (
{ if (e.target === e.currentTarget) onClose(); }} role="dialog" aria-modal="true" aria-label={(d && (d.heading || d.title)) || cur.label || 'المادة'}>
{body}
{kind === 'article' && <>
step(-1)} disabled={!d || d.ord <= 1 || st.loading}>→ السابقة
step(1)} disabled={!d || d.ord >= d.n_heads || st.loading}>التالية ←
{d && d.sharh &&
setCur({ kind: 'sharh', did: d.did, an: d.sharh.an, back: cur })}>📘 الشرح الرسمي }
d && lxOpenFullDoc(d.did, d.ord)}>الوثيقة كاملة
>}
{kind === 'sharh' && <>
step(-1)} disabled={!d || d.an <= 1 || st.loading}>→ السابقة
step(1)} disabled={!d || st.loading}>التالية ←
setCur(cur.back && cur.back.did === cur.did && d && cur.back.an === d.an ? cur.back : { did: cur.did, an: d ? d.an : cur.an })}>نص المادة
>}
{kind === 'guide' && d && d.url &&
المصدر الرسمي ↗ }
{toast &&
{toast}
}
);
}
// guides1: تبويبُ «الشروح والأدلة الرسمية» في قارئ النظام
function LxGuides({ gd }) {
const perArt = gd.sharh_arts && gd.sharh_arts.length;
return (
شروحٌ وأدلّةٌ صادرةٌ عن الجهاتِ المختصّة لهذا النظام.{perArt ? ' ولكلّ مادّةٍ مشروحةٍ زرُّ «📘 الشرح الرسمي» في النصّ.' : ''}
{gd.guides.map(g => (
{g.title} {g.issuer}{g.n_arts ? ' · مشروحٌ مادّةً مادّة (' + lxNum(g.n_arts) + ' مادة)' : ''}
{g.n_arts > 0 && g.parts > 1
?
window.lxOpenArticle({ kind: 'sharh', did: gd.law, an: gd.sharh_arts[0] })}>ابدأ من المادة الأولى
:
window.lxOpenArticle({ kind: 'guide', id: g.id, label: g.title })}>اقرأ }
{g.url &&
المصدر ↗ }
))}
);
}
function LxArticleSheetHost({ req }) {
const [open, setOpen] = React.useState(true);
React.useEffect(() => {
// مدخلٌ في السجلّ: زرُّ «رجوع» في الجوال يُغلقُ اللوحةَ بدلَ مغادرةِ الشاشة
try { if (!(history.state && history.state.lxSheet)) history.pushState({ ...(history.state || {}), lxSheet: 1 }, ''); } catch (_) {}
const onPop = () => { if (!(history.state && history.state.lxSheet)) setOpen(false); };
window.addEventListener('popstate', onPop);
return () => window.removeEventListener('popstate', onPop);
}, []);
if (!open) return null;
return
{ if (history.state && history.state.lxSheet) history.back(); else setOpen(false); }} />;
}
let lxSheetRoot = null, lxSheetSeq = 0;
window.lxOpenArticle = function (req) {
if (!req || !(req.did || req.id)) return false;
let host = document.getElementById('lx-art-sheet');
if (!host) { host = document.createElement('div'); host.id = 'lx-art-sheet'; document.body.appendChild(host); lxSheetRoot = ReactDOM.createRoot(host); }
lxSheetRoot.render( );
return true;
};
function LawsView() {
const [route, setRoute] = React.useState(() => lxReadRoute());
const lxSec = (s) => (s && s !== 'books' ? s : 'laws');
const [section, setSection] = React.useState(() => lxSec(lxReadRoute().sec));
const [meta, setMeta] = React.useState(null);
const [metaErr, setMetaErr] = React.useState(null);
const [prefs, setPrefs] = React.useState(() => ({ fs: 17, theme: 'paper', focus: false, ...lxLoad('lx-prefs', {}) }));
React.useEffect(() => {
lxGet('/api/laws/meta').then(setMeta).catch(e => setMetaErr(e.message));
const f = () => { const r = lxReadRoute(); setRoute(r); setSection(lxSec(r.sec)); };
window.addEventListener('popstate', f);
return () => window.removeEventListener('popstate', f);
}, []);
const go = (next, replace) => { lxWriteRoute(next, replace); setRoute(next); };
const openDoc = (did, ord, an, tab) => go({ doc: did, art: ord || undefined, an: an || undefined, tab: tab || undefined });
const search = (q) => go({ q });
const browse = (f) => go({ browse: '1', ...f });
const onSection = (s) => { setSection(s); go(s === 'laws' ? {} : { sec: s }); };
let body;
if (route.doc) body = history.length > 1 ? history.back() : go({})} />;
else if (section === 'treaties') body = ;
else if (section !== 'laws') body = ;
else if (route.q || route.browse) body = ;
else body = (
);
return (
{body}
);
}
window.LawsView = LawsView;