ملف آخر
Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
هذا الالتزام موجود في:
908
src/App.tsx
Normal file
908
src/App.tsx
Normal file
@@ -0,0 +1,908 @@
|
||||
/**
|
||||
* نقطة الإدخال وواجهة المستخدم (Main Application Component) - SteganoPixel
|
||||
* =========================================================================
|
||||
* الجزء الأول من الشرح الهيكلي:
|
||||
* يمثل هذا الملف الواجهة التفاعلية الشاملة لتطبيق "SteganoPixel"، حيث يجمع بين طبقة العرض (React & Tailwind CSS)
|
||||
* وطبقة المعالجة المنطقية (Steganography Engine). يتيح المكون للمستخدمين التنقل بسلاسة بين نمطين أساسيين: نمط "الإخفاء"
|
||||
* لدمج النصوص السرية داخل الصور المرفوعة، ونمط "الاستخراج/التنظيف" لقراءة النصوص المضمنة مسبقاً أو تدميرها نهائياً.
|
||||
* يتم إدارة جميع الحالات التفاعلية (معاينة الصور، أخطاء الإدخال، تقدم العمليات، وحالة النسخ إلى الحافظة) محلياً داخل المتصفح.
|
||||
*
|
||||
* الجزء الثاني من الشرح الهيكلي:
|
||||
* يتفاعل المكون مع محرك الإخفاء الرقمي (`src/utils/steganography.ts`) من خلال تحويل ملفات الصور المرفوعة (File API)
|
||||
* إلى عناصر مرئية (HTMLImageElement) ثم رسمها على كائن `<canvas>` في الذاكرة لاستخلاص مصفوفة البكسلات الخام (`ImageData`).
|
||||
* يتم تمرير هذه المصفوفة إلى دوال التشفير أو فك التشفير، ثم يعاد تجميع النتيجة في ملف `Blob` بصيغة `image/png` وتوليد
|
||||
* رابط محلي (`URL.createObjectURL`) لتمكين المستخدم من تنزيل الصورة الناتجة بأمان تام ودون أي اتصال بخوادم خارجية.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useState, type ChangeEvent, type DragEvent } from "react";
|
||||
import {
|
||||
embedTextInImageData,
|
||||
extractHiddenTextFromImageData,
|
||||
getImageCapacityBytes,
|
||||
removeHiddenTextFromImageData,
|
||||
type ExtractedHiddenText,
|
||||
type HiddenTextMetadata,
|
||||
} from "./utils/steganography";
|
||||
|
||||
type Mode = "hide" | "extract";
|
||||
|
||||
type SelectedImage = {
|
||||
file: File;
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type GeneratedAsset = {
|
||||
url: string;
|
||||
fileName: string;
|
||||
size: number;
|
||||
width: number;
|
||||
height: number;
|
||||
messageBytes?: number;
|
||||
};
|
||||
|
||||
type UploaderProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
selectedImage: SelectedImage | null;
|
||||
onSelectFile: (file: File) => void;
|
||||
onClear: () => void;
|
||||
hint?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* تنسيق حجم البايتات إلى نصوص مقروءة للبشر
|
||||
*/
|
||||
function formatBytes(bytes: number) {
|
||||
if (bytes < 1024) return `${bytes} بايت`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} كيلوبايت`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(2)} ميجابايت`;
|
||||
}
|
||||
|
||||
/**
|
||||
* استخراج الاسم الأساسي للملف بدون الامتداد
|
||||
*/
|
||||
function baseName(fileName: string) {
|
||||
return fileName.replace(/\.[^.]+$/, "") || "image";
|
||||
}
|
||||
|
||||
/**
|
||||
* إطلاق عملية التنزيل برمجياً لضمان توافقية جميع المتصفحات
|
||||
*/
|
||||
function triggerDownload(url: string, fileName: string) {
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
link.rel = "noopener";
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* تحميل الصورة ككائن HTMLImageElement لاستخلاص أبعادها بدقة
|
||||
*/
|
||||
function loadImage(src: string) {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = () => reject(new Error("تعذر تحميل الصورة المحددة."));
|
||||
image.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* إعداد وتحليل الصورة المحددة بواسطة المستخدم
|
||||
*/
|
||||
async function prepareSelectedImage(file: File): Promise<SelectedImage> {
|
||||
if (!file.type.startsWith("image/")) {
|
||||
throw new Error("الملف المحدد ليس صورة صالحة.");
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
try {
|
||||
const image = await loadImage(url);
|
||||
return {
|
||||
file,
|
||||
url,
|
||||
width: image.naturalWidth,
|
||||
height: image.naturalHeight,
|
||||
};
|
||||
} catch (error) {
|
||||
URL.revokeObjectURL(url);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* إنشاء عنصر Canvas في الذاكرة لنقل بيانات الصورة إليه
|
||||
*/
|
||||
function createCanvasFromImage(image: HTMLImageElement) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
if (!context) {
|
||||
throw new Error("تعذر الوصول إلى محرك الرسم داخل المتصفح.");
|
||||
}
|
||||
|
||||
context.drawImage(image, 0, 0);
|
||||
return { canvas, context };
|
||||
}
|
||||
|
||||
/**
|
||||
* تحويل محتوى Canvas إلى Blob بصيغة PNG لضمان حفظ البتات بدون ضياع
|
||||
*/
|
||||
function canvasToPngBlob(canvas: HTMLCanvasElement) {
|
||||
return new Promise<Blob>((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
reject(new Error("تعذر إنشاء ملف الصورة النهائي."));
|
||||
return;
|
||||
}
|
||||
resolve(blob);
|
||||
},
|
||||
"image/png",
|
||||
1,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* مكون عرض الإحصائيات المصغرة (Pill Component)
|
||||
*/
|
||||
function StatPill({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/70 bg-white/80 px-4 py-3 shadow-sm shadow-slate-200/60 backdrop-blur">
|
||||
<div className="text-xs font-medium text-slate-500">{label}</div>
|
||||
<div className="mt-1 text-sm font-semibold text-slate-900" dir="ltr">
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* مكون رفع و اختيار الصور (Drag and Drop Uploader)
|
||||
*/
|
||||
function UploadCard({ title, description, selectedImage, onSelectFile, onClear, hint }: UploaderProps) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const inputId = `${title.replace(/\s+/g, "-")}-input`;
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) onSelectFile(file);
|
||||
event.currentTarget.value = "";
|
||||
};
|
||||
|
||||
const handleDrop = (event: DragEvent<HTMLLabelElement>) => {
|
||||
event.preventDefault();
|
||||
setIsDragging(false);
|
||||
|
||||
const file = event.dataTransfer.files?.[0];
|
||||
if (file) onSelectFile(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-3xl border border-slate-200 bg-white/85 p-4 shadow-lg shadow-slate-200/50">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-slate-900">{title}</h3>
|
||||
<p className="mt-1 text-sm leading-6 text-slate-600">{description}</p>
|
||||
</div>
|
||||
{selectedImage ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClear}
|
||||
className="rounded-full border border-slate-200 px-3 py-1.5 text-xs font-medium text-slate-600 transition hover:border-rose-200 hover:text-rose-600"
|
||||
>
|
||||
إزالة الصورة
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-4 lg:grid-cols-[1fr_220px]">
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
setIsDragging(true);
|
||||
}}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
onDrop={handleDrop}
|
||||
className={`group flex min-h-[220px] cursor-pointer flex-col items-center justify-center rounded-3xl border-2 border-dashed px-6 py-8 text-center transition ${
|
||||
isDragging
|
||||
? "border-violet-400 bg-violet-50"
|
||||
: "border-slate-200 bg-slate-50/80 hover:border-violet-300 hover:bg-violet-50/70"
|
||||
}`}
|
||||
>
|
||||
<input id={inputId} type="file" accept="image/*" className="hidden" onChange={handleChange} />
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-violet-500 to-indigo-600 text-white shadow-lg shadow-violet-200/80">
|
||||
<svg viewBox="0 0 24 24" className="h-7 w-7 fill-none stroke-current" strokeWidth="1.8">
|
||||
<path d="M12 16V5" strokeLinecap="round" />
|
||||
<path d="m7 10 5-5 5 5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M5 19h14" strokeLinecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="mt-4 text-lg font-semibold text-slate-900">
|
||||
{selectedImage ? "استبدال الصورة الحالية" : "اسحب الصورة هنا أو اضغط للاختيار"}
|
||||
</div>
|
||||
<div className="mt-2 max-w-md text-sm leading-6 text-slate-500">
|
||||
يدعم التطبيق صور PNG وJPG وWEBP وغيرها. تتم المعالجة محليًا داخل المتصفح فقط.
|
||||
</div>
|
||||
{hint ? <div className="mt-3 text-xs font-medium text-violet-700">{hint}</div> : null}
|
||||
</label>
|
||||
|
||||
<div className="overflow-hidden rounded-3xl border border-slate-200 bg-slate-50/90">
|
||||
{selectedImage ? (
|
||||
<>
|
||||
<img
|
||||
src={selectedImage.url}
|
||||
alt={selectedImage.file.name}
|
||||
className="h-[220px] w-full bg-white object-contain"
|
||||
/>
|
||||
<div className="space-y-2 border-t border-slate-200 px-4 py-4 text-sm text-slate-600">
|
||||
<div className="font-medium text-slate-800">{selectedImage.file.name}</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span>الأبعاد</span>
|
||||
<span dir="ltr">
|
||||
{selectedImage.width} × {selectedImage.height}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span>الحجم</span>
|
||||
<span dir="ltr">{formatBytes(selectedImage.file.size)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span>النوع</span>
|
||||
<span dir="ltr">{selectedImage.file.type || "image/*"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full min-h-[220px] flex-col items-center justify-center gap-3 px-5 text-center text-sm text-slate-500">
|
||||
<div className="rounded-2xl bg-white p-4 shadow-sm">
|
||||
<svg viewBox="0 0 24 24" className="h-8 w-8 fill-none stroke-slate-400" strokeWidth="1.7">
|
||||
<rect x="3" y="4" width="18" height="16" rx="2" />
|
||||
<path d="m7 15 3-3 3 3 4-5 2 3" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<circle cx="9" cy="9" r="1.2" fill="currentColor" className="text-slate-400" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>ستظهر هنا معاينة الصورة بعد اختيارها.</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* مكون عرض النتيجة وتنزيلها (Download and Preview Card)
|
||||
*/
|
||||
function DownloadCard({
|
||||
title,
|
||||
description,
|
||||
asset,
|
||||
metadata,
|
||||
buttonLabel,
|
||||
onDownload,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
asset: GeneratedAsset | null;
|
||||
metadata?: HiddenTextMetadata | ExtractedHiddenText | null;
|
||||
buttonLabel: string;
|
||||
onDownload: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-3xl border border-slate-200 bg-white/85 p-4 shadow-lg shadow-slate-200/50">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-slate-900">{title}</h3>
|
||||
<p className="mt-1 text-sm leading-6 text-slate-600">{description}</p>
|
||||
</div>
|
||||
|
||||
{asset ? (
|
||||
<div className="mt-4 grid gap-4 lg:grid-cols-[1fr_260px]">
|
||||
<div className="overflow-hidden rounded-3xl border border-slate-200 bg-slate-50/80">
|
||||
<img src={asset.url} alt={asset.fileName} className="max-h-[340px] w-full bg-white object-contain" />
|
||||
</div>
|
||||
<div className="space-y-4 rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-slate-900">{asset.fileName}</div>
|
||||
<div className="mt-1 text-sm text-slate-500">تم إنشاء نسخة PNG جديدة جاهزة للتنزيل والاستخدام.</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1">
|
||||
<StatPill label="الحجم النهائي" value={formatBytes(asset.size)} />
|
||||
<StatPill label="الأبعاد" value={`${asset.width} × ${asset.height}`} />
|
||||
{typeof asset.messageBytes === "number" ? (
|
||||
<StatPill label="النص المضمّن" value={formatBytes(asset.messageBytes)} />
|
||||
) : null}
|
||||
{metadata ? <StatPill label="المساحة المستخدمة" value={formatBytes(metadata.usedBytes)} /> : null}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDownload}
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-2xl bg-slate-900 px-4 py-3 text-sm font-semibold text-white transition hover:bg-slate-800"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="h-4 w-4 fill-none stroke-current" strokeWidth="1.8">
|
||||
<path d="M12 4v11" strokeLinecap="round" />
|
||||
<path d="m7 10 5 5 5-5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M5 20h14" strokeLinecap="round" />
|
||||
</svg>
|
||||
{buttonLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 rounded-3xl border border-dashed border-slate-200 bg-slate-50/80 px-6 py-12 text-center text-sm leading-7 text-slate-500">
|
||||
لا توجد نتيجة حتى الآن. عند تنفيذ العملية ستظهر هنا معاينة النسخة الجديدة مع زر التنزيل.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [mode, setMode] = useState<Mode>("hide");
|
||||
|
||||
// حالات الإخفاء (Embedding states)
|
||||
const [encodeImage, setEncodeImage] = useState<SelectedImage | null>(null);
|
||||
const [encodeText, setEncodeText] = useState("");
|
||||
const [encodeStatus, setEncodeStatus] = useState("");
|
||||
const [encodeError, setEncodeError] = useState("");
|
||||
const [encodeBusy, setEncodeBusy] = useState(false);
|
||||
const [encodedAsset, setEncodedAsset] = useState<GeneratedAsset | null>(null);
|
||||
const [encodedMetadata, setEncodedMetadata] = useState<HiddenTextMetadata | null>(null);
|
||||
|
||||
// حالات الاستخراج والتنظيف (Extracting & Cleaning states)
|
||||
const [decodeImage, setDecodeImage] = useState<SelectedImage | null>(null);
|
||||
const [decodeStatus, setDecodeStatus] = useState("");
|
||||
const [decodeError, setDecodeError] = useState("");
|
||||
const [decodeBusy, setDecodeBusy] = useState(false);
|
||||
const [cleanBusy, setCleanBusy] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [extracted, setExtracted] = useState<ExtractedHiddenText | null>(null);
|
||||
const [cleanedAsset, setCleanedAsset] = useState<GeneratedAsset | null>(null);
|
||||
|
||||
// تنظيف عناوين الذاكرة عند تغيير الصور
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (encodeImage?.url) URL.revokeObjectURL(encodeImage.url);
|
||||
};
|
||||
}, [encodeImage]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (decodeImage?.url) URL.revokeObjectURL(decodeImage.url);
|
||||
};
|
||||
}, [decodeImage]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (encodedAsset?.url) URL.revokeObjectURL(encodedAsset.url);
|
||||
};
|
||||
}, [encodedAsset]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (cleanedAsset?.url) URL.revokeObjectURL(cleanedAsset.url);
|
||||
};
|
||||
}, [cleanedAsset]);
|
||||
|
||||
// مؤقت إعادة تعيين حالة النسخ
|
||||
useEffect(() => {
|
||||
if (!copied) return;
|
||||
const timeout = window.setTimeout(() => setCopied(false), 1800);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [copied]);
|
||||
|
||||
const messageBytes = useMemo(() => new TextEncoder().encode(encodeText).length, [encodeText]);
|
||||
const capacityBytes = encodeImage ? getImageCapacityBytes(encodeImage.width, encodeImage.height) : 0;
|
||||
const usagePercent = capacityBytes > 0 ? Math.min(100, (messageBytes / capacityBytes) * 100) : 0;
|
||||
|
||||
const handleEncodeImageSelect = async (file: File) => {
|
||||
setEncodeError("");
|
||||
setEncodeStatus("");
|
||||
setEncodedAsset(null);
|
||||
setEncodedMetadata(null);
|
||||
|
||||
try {
|
||||
const selected = await prepareSelectedImage(file);
|
||||
setEncodeImage(selected);
|
||||
} catch (error) {
|
||||
setEncodeError(error instanceof Error ? error.message : "حدث خطأ أثناء تحميل الصورة.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDecodeImageSelect = async (file: File) => {
|
||||
setDecodeError("");
|
||||
setDecodeStatus("");
|
||||
setExtracted(null);
|
||||
setCleanedAsset(null);
|
||||
setCopied(false);
|
||||
|
||||
try {
|
||||
const selected = await prepareSelectedImage(file);
|
||||
setDecodeImage(selected);
|
||||
} catch (error) {
|
||||
setDecodeError(error instanceof Error ? error.message : "حدث خطأ أثناء تحميل الصورة.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEncode = async () => {
|
||||
if (!encodeImage) {
|
||||
setEncodeError("اختر صورة أولًا قبل محاولة تضمين النص.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!encodeText) {
|
||||
setEncodeError("أدخل النص الذي تريد تضمينه داخل الصورة.");
|
||||
return;
|
||||
}
|
||||
|
||||
setEncodeBusy(true);
|
||||
setEncodeError("");
|
||||
setEncodeStatus("جارٍ تضمين النص داخل الصورة...");
|
||||
|
||||
try {
|
||||
const image = await loadImage(encodeImage.url);
|
||||
const { canvas, context } = createCanvasFromImage(image);
|
||||
const source = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const { imageData, metadata } = embedTextInImageData(source, encodeText);
|
||||
|
||||
context.putImageData(imageData, 0, 0);
|
||||
const blob = await canvasToPngBlob(canvas);
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
setEncodedMetadata(metadata);
|
||||
setEncodedAsset({
|
||||
url,
|
||||
fileName: `${baseName(encodeImage.file.name)}-hidden.png`,
|
||||
size: blob.size,
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
messageBytes: metadata.messageBytes,
|
||||
});
|
||||
setEncodeStatus("تم إنشاء نسخة PNG جديدة تحمل النص المخفي بنجاح.");
|
||||
} catch (error) {
|
||||
setEncodeError(error instanceof Error ? error.message : "تعذر تضمين النص داخل الصورة.");
|
||||
setEncodeStatus("");
|
||||
} finally {
|
||||
setEncodeBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExtract = async () => {
|
||||
if (!decodeImage) {
|
||||
setDecodeError("اختر صورة أولًا لاستخراج النص المخفي.");
|
||||
return;
|
||||
}
|
||||
|
||||
setDecodeBusy(true);
|
||||
setDecodeError("");
|
||||
setDecodeStatus("جارٍ فحص الصورة واستخراج النص...");
|
||||
|
||||
try {
|
||||
const image = await loadImage(decodeImage.url);
|
||||
const { canvas, context } = createCanvasFromImage(image);
|
||||
const source = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const result = extractHiddenTextFromImageData(source);
|
||||
|
||||
setExtracted(result);
|
||||
setDecodeStatus("تم العثور على النص المخفي واستخراجه بنجاح.");
|
||||
} catch (error) {
|
||||
setExtracted(null);
|
||||
setDecodeError(error instanceof Error ? error.message : "تعذر استخراج النص من الصورة.");
|
||||
setDecodeStatus("");
|
||||
} finally {
|
||||
setDecodeBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClean = async () => {
|
||||
if (!decodeImage) {
|
||||
setDecodeError("اختر صورة أولًا ثم قم بتنظيفها من النص المخفي.");
|
||||
return;
|
||||
}
|
||||
|
||||
setCleanBusy(true);
|
||||
setDecodeError("");
|
||||
setDecodeStatus("جارٍ إنشاء نسخة نظيفة من الصورة...");
|
||||
|
||||
try {
|
||||
const image = await loadImage(decodeImage.url);
|
||||
const { canvas, context } = createCanvasFromImage(image);
|
||||
const source = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const { imageData, metadata } = removeHiddenTextFromImageData(source);
|
||||
|
||||
context.putImageData(imageData, 0, 0);
|
||||
const blob = await canvasToPngBlob(canvas);
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
setExtracted(metadata);
|
||||
setCleanedAsset({
|
||||
url,
|
||||
fileName: `${baseName(decodeImage.file.name)}-clean.png`,
|
||||
size: blob.size,
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
});
|
||||
setDecodeStatus("تم إنشاء نسخة نظيفة بدون النص المخفي. يمكنك تنزيلها الآن.");
|
||||
} catch (error) {
|
||||
setCleanedAsset(null);
|
||||
setDecodeError(error instanceof Error ? error.message : "تعذر تنظيف الصورة.");
|
||||
setDecodeStatus("");
|
||||
} finally {
|
||||
setCleanBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!extracted?.message) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(extracted.message);
|
||||
setCopied(true);
|
||||
} catch {
|
||||
setDecodeError("تعذر نسخ النص تلقائيًا. يمكنك نسخه يدويًا من مربع النص.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
dir="rtl"
|
||||
className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(124,58,237,0.16),_transparent_34%),linear-gradient(180deg,_#f8fafc,_#eef2ff_45%,_#ffffff)] text-slate-900"
|
||||
>
|
||||
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<header className="overflow-hidden rounded-[2rem] border border-white/70 bg-white/80 p-6 shadow-2xl shadow-violet-100/80 backdrop-blur sm:p-8">
|
||||
<div className="grid gap-8 lg:grid-cols-[1.3fr_0.9fr] lg:items-center">
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-2.5 rounded-full border border-violet-200 bg-violet-50 px-4 py-2 text-sm font-semibold text-violet-700">
|
||||
<span className="flex h-3 w-3 items-center justify-center rounded-full bg-violet-500 text-[8px] text-white">
|
||||
S
|
||||
</span>
|
||||
<span>ستيجانو بكسل (SteganoPixel)</span>
|
||||
</div>
|
||||
<h1 className="mt-5 text-3xl font-black tracking-tight text-slate-950 sm:text-5xl">
|
||||
ضع النص داخل أي صورة واستخرجه أو أزله من نفس التطبيق
|
||||
</h1>
|
||||
<p className="mt-4 max-w-3xl text-base leading-8 text-slate-600 sm:text-lg">
|
||||
تطبيق ويب عربي متقدم لإخفاء النصوص السرية داخل الصور الرقمية بأسلوب غير ملحوظ بصرياً (LSB Steganography)،
|
||||
مع إمكانية قراءة النصوص المضمنة لاحقاً أو محوها بالكامل لتأمين خصوصيتك. تعمل جميع خوارزميات التشفير
|
||||
مباشرة داخل متصفحك دون إرسال أي ملفات إلى خوادم خارجية.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-3 text-sm text-slate-600">
|
||||
<div className="rounded-full border border-slate-200 bg-slate-50 px-4 py-2">
|
||||
حفظ بصيغة PNG Lossless للحفاظ على البتات الدقيقة
|
||||
</div>
|
||||
<div className="rounded-full border border-slate-200 bg-slate-50 px-4 py-2">
|
||||
يدعم التضمين · الاستخراج · إزالة الدمج (التنظيف)
|
||||
</div>
|
||||
<div className="rounded-full border border-slate-200 bg-slate-50 px-4 py-2">
|
||||
خصوصية تامة 100% بدون اتصال بالإنترنت
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<StatPill label="خوارزمية الدمج" value="3-Channel LSB" />
|
||||
<StatPill label="بيئة المعالجة" value="Client-Side Browser" />
|
||||
<StatPill label="دعم التصدير" value="PNG TrueColor (Lossless)" />
|
||||
<StatPill label="مستوى الأمان" value="Zero Server Uploads" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mt-8 grid gap-6 xl:grid-cols-[1.55fr_0.85fr]">
|
||||
<section className="rounded-[2rem] border border-white/70 bg-white/80 p-4 shadow-2xl shadow-slate-200/60 backdrop-blur sm:p-6">
|
||||
<div className="flex flex-wrap gap-3 rounded-3xl bg-slate-100/80 p-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode("hide")}
|
||||
className={`flex-1 rounded-2xl px-4 py-3 text-sm font-semibold transition sm:flex-none sm:min-w-[180px] ${
|
||||
mode === "hide"
|
||||
? "bg-slate-900 text-white shadow-lg shadow-slate-300"
|
||||
: "bg-transparent text-slate-600 hover:bg-white"
|
||||
}`}
|
||||
>
|
||||
تضمين نص سري داخل صورة
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode("extract")}
|
||||
className={`flex-1 rounded-2xl px-4 py-3 text-sm font-semibold transition sm:flex-none sm:min-w-[180px] ${
|
||||
mode === "extract"
|
||||
? "bg-slate-900 text-white shadow-lg shadow-slate-300"
|
||||
: "bg-transparent text-slate-600 hover:bg-white"
|
||||
}`}
|
||||
>
|
||||
استخراج النص أو تنظيف الصورة
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{mode === "hide" ? (
|
||||
<div className="mt-6 space-y-6">
|
||||
<UploadCard
|
||||
title="1) اختر الصورة الأصلية (الناقل)"
|
||||
description="ارفع الصورة التي تريد دمج النص داخلها. سيقوم التطبيق بقراءة أبعادها وحساب السعة التخزينية المتاحة لتضمين رسالتك."
|
||||
selectedImage={encodeImage}
|
||||
onSelectFile={(file) => {
|
||||
void handleEncodeImageSelect(file);
|
||||
}}
|
||||
onClear={() => {
|
||||
setEncodeImage(null);
|
||||
setEncodedAsset(null);
|
||||
setEncodedMetadata(null);
|
||||
setEncodeStatus("");
|
||||
setEncodeError("");
|
||||
}}
|
||||
hint={
|
||||
encodeImage?.file.type.includes("jpeg")
|
||||
? "تنبيه: سيتم تحويل صورتك الـ JPG إلى PNG عند الحفظ لضمان عدم تلف البيانات المخفية بسبب ضغط الـ JPG الضياعي."
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="rounded-3xl border border-slate-200 bg-white/85 p-4 shadow-lg shadow-slate-200/50">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-slate-900">2) اكتب النص السري المراد إخفاؤه</h3>
|
||||
<p className="mt-1 text-sm leading-6 text-slate-600">
|
||||
تُشفّر الرسالة رقمياً وتُوزّع بانتظام على البتات الأقل أهمية للألوان (RGB)، ما يحافظ على النقاء
|
||||
البصري.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-2 text-sm text-slate-600"
|
||||
dir="ltr"
|
||||
>
|
||||
{messageBytes} / {capacityBytes || 0} bytes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
value={encodeText}
|
||||
onChange={(event) => setEncodeText(event.target.value)}
|
||||
placeholder="اكتب رسالتك السرية هنا..."
|
||||
className="mt-4 min-h-[180px] w-full rounded-3xl border border-slate-200 bg-slate-50/80 px-4 py-4 text-sm leading-7 text-slate-800 outline-none transition placeholder:text-slate-400 focus:border-violet-400 focus:bg-white focus:ring-4 focus:ring-violet-100"
|
||||
/>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="flex items-center justify-between text-xs font-medium text-slate-500">
|
||||
<span>نسبة استهلاك السعة التخزينية</span>
|
||||
<span dir="ltr">{usagePercent.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="h-3 overflow-hidden rounded-full bg-slate-100">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all ${
|
||||
messageBytes > capacityBytes && capacityBytes > 0
|
||||
? "bg-rose-500"
|
||||
: "bg-gradient-to-r from-violet-500 to-indigo-600"
|
||||
}`}
|
||||
style={{ width: `${Math.min(100, usagePercent)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<StatPill label="السعة الكلية المتاحة" value={formatBytes(capacityBytes)} />
|
||||
<StatPill label="حجم الرسالة الحالي" value={formatBytes(messageBytes)} />
|
||||
<StatPill label="صيغة التصدير" value="PNG Lossless" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap gap-3">
|
||||
<button
|
||||
type="button"
|
||||
disabled={encodeBusy}
|
||||
onClick={() => {
|
||||
void handleEncode();
|
||||
}}
|
||||
className="inline-flex items-center justify-center rounded-2xl bg-violet-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-violet-200 transition hover:bg-violet-700 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{encodeBusy ? "جارٍ التشفير والدمج..." : "إنشاء الصورة المضمّنة"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEncodeText("");
|
||||
setEncodeStatus("");
|
||||
setEncodeError("");
|
||||
}}
|
||||
className="inline-flex items-center justify-center rounded-2xl border border-slate-200 bg-white px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-slate-300 hover:bg-slate-50"
|
||||
>
|
||||
مسح النص
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{encodeStatus ? (
|
||||
<div className="mt-4 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-700">
|
||||
{encodeStatus}
|
||||
</div>
|
||||
) : null}
|
||||
{encodeError ? (
|
||||
<div className="mt-4 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm font-medium text-rose-700">
|
||||
{encodeError}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<DownloadCard
|
||||
title="3) النتيجة النهائية الجاهزة"
|
||||
description="انقر أدناه لتنزيل نسختك الجديدة من الصورة. تحتوي هذه الصورة الآن على النص السري المضمن وبأعلى جودة بصرية."
|
||||
asset={encodedAsset}
|
||||
metadata={encodedMetadata}
|
||||
buttonLabel="تنزيل الصورة المضمّنة (PNG)"
|
||||
onDownload={() => {
|
||||
if (encodedAsset) triggerDownload(encodedAsset.url, encodedAsset.fileName);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 space-y-6">
|
||||
<UploadCard
|
||||
title="1) اختر الصورة المراد فحصها أو تنظيفها"
|
||||
description="ارفع الصورة المشتبه باحتوائها على نص مخفي. يمكنك استخراج وقراءة محتواها، أو محو البيانات السرية لإنتاج نسخة نظيفة."
|
||||
selectedImage={decodeImage}
|
||||
onSelectFile={(file) => {
|
||||
void handleDecodeImageSelect(file);
|
||||
}}
|
||||
onClear={() => {
|
||||
setDecodeImage(null);
|
||||
setExtracted(null);
|
||||
setCleanedAsset(null);
|
||||
setDecodeStatus("");
|
||||
setDecodeError("");
|
||||
setCopied(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="rounded-3xl border border-slate-200 bg-white/85 p-4 shadow-lg shadow-slate-200/50">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
type="button"
|
||||
disabled={decodeBusy}
|
||||
onClick={() => {
|
||||
void handleExtract();
|
||||
}}
|
||||
className="inline-flex items-center justify-center rounded-2xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-200 transition hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{decodeBusy ? "جارٍ الفحص والاستخراج..." : "استخراج وقراءة النص السري"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={cleanBusy}
|
||||
onClick={() => {
|
||||
void handleClean();
|
||||
}}
|
||||
className="inline-flex items-center justify-center rounded-2xl border border-slate-200 bg-white px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-slate-300 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{cleanBusy ? "جارٍ محو البيانات..." : "إنشاء نسخة نظيفة خالية من النصوص"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{decodeStatus ? (
|
||||
<div className="mt-4 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-700">
|
||||
{decodeStatus}
|
||||
</div>
|
||||
) : null}
|
||||
{decodeError ? (
|
||||
<div className="mt-4 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm font-medium text-rose-700">
|
||||
{decodeError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-5 rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-slate-900">محتوى النص المستخرج</h3>
|
||||
<p className="mt-1 text-sm leading-6 text-slate-600">
|
||||
إذا تم العثور على حزمة بيانات صحيحة في الصورة، سيتم تفكيكها وعرضها هنا.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!extracted?.message}
|
||||
onClick={() => {
|
||||
void handleCopy();
|
||||
}}
|
||||
className="rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:border-slate-300 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{copied ? "تم النسخ بنجاح" : "نسخ النص"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
readOnly
|
||||
value={extracted?.message ?? ""}
|
||||
placeholder="بعد رفع الصورة والنقر على «استخراج وقراءة النص السري»، ستظهر الرسالة المستخرجة هنا إن وجدت."
|
||||
className="mt-4 min-h-[180px] w-full rounded-3xl border border-slate-200 bg-white px-4 py-4 text-sm leading-7 text-slate-800 outline-none placeholder:text-slate-400"
|
||||
/>
|
||||
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-3">
|
||||
<StatPill label="حجم النص المستخرج" value={formatBytes(extracted?.messageBytes ?? 0)} />
|
||||
<StatPill label="حجم الحزمة المشفرة" value={formatBytes(extracted?.usedBytes ?? 0)} />
|
||||
<StatPill label="أبعاد الصورة" value={extracted ? `${extracted.width} × ${extracted.height}` : "—"} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DownloadCard
|
||||
title="2) النسخة النظيفة المُعقمة"
|
||||
description="إذا قمت بتنظيف الصورة، يمكنك تنزيل النسخة الجديدة أدناه. تم تصفير جميع بتات البيانات المضمنة فيها لضمان عدم إمكانية استخراجها مجدداً."
|
||||
asset={cleanedAsset}
|
||||
metadata={extracted}
|
||||
buttonLabel="تنزيل الصورة النظيفة (PNG)"
|
||||
onDownload={() => {
|
||||
if (cleanedAsset) triggerDownload(cleanedAsset.url, cleanedAsset.fileName);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<aside className="space-y-6">
|
||||
<div className="rounded-[2rem] border border-white/70 bg-white/80 p-6 shadow-2xl shadow-slate-200/60 backdrop-blur">
|
||||
<h2 className="text-xl font-bold text-slate-950">آلية عمل التطبيق (LSB Architecture)</h2>
|
||||
<div className="mt-5 space-y-4">
|
||||
<div className="rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
|
||||
<div className="text-sm font-bold text-slate-900">1. التشفير والتضمين (Embedding)</div>
|
||||
<p className="mt-2 text-sm leading-7 text-slate-600">
|
||||
يحول التطبيق حروفك إلى بايتات رقمية، ويضيف إليها توقيعاً سحرياً (`TXTIMG1`) وطول الرسالة، ثم يضعها
|
||||
بانتظام في البت الأقل أهمية (LSB) لقنوات الألوان (RGB) لكل بكسل، ما يحافظ على دقة الألوان الأصلية.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
|
||||
<div className="text-sm font-bold text-slate-900">2. القراءة والاستخراج (Extracting)</div>
|
||||
<p className="mt-2 text-sm leading-7 text-slate-600">
|
||||
عند الفحص، يقرأ المحرك البكسلات بالترتيب باحثاً عن التوقيع السحري. إذا تطابق التوقيع، يقرأ طول
|
||||
الرسالة ويستخرج البتات المتبقية ليعيد تكوين النص الأصلي بدقة متناهية.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
|
||||
<div className="text-sm font-bold text-slate-900">3. محو البيانات (Sanitization)</div>
|
||||
<p className="mt-2 text-sm leading-7 text-slate-600">
|
||||
أداة التنظيف تقوم بحساب البتات التي تم استخدامها في الصورة وتصفرها تماماً (`& 0xFE`)، ما يعيد
|
||||
البكسلات لحالة عشوائية طبيعية ويمنع أي محاولة مستقبلية لاستعادة النص.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[2rem] border border-white/70 bg-white/80 p-6 shadow-2xl shadow-violet-100/70 backdrop-blur">
|
||||
<h2 className="text-xl font-bold text-slate-950">ضوابط الحفاظ على الجودة</h2>
|
||||
<ul className="mt-4 space-y-3 text-sm leading-7 text-slate-600">
|
||||
<li className="rounded-2xl border border-slate-200 bg-slate-50/80 px-4 py-3">
|
||||
يتم تصدير الصور حصراً بصيغة <span className="font-semibold text-slate-900">PNG Lossless</span>.
|
||||
حفظها كـ JPG أو إرسالها عبر الواتساب/تليجرام بوضعية الضغط العادي يؤدي إلى تدمير البتات المخفية.
|
||||
</li>
|
||||
<li className="rounded-2xl border border-slate-200 bg-slate-50/80 px-4 py-3">
|
||||
كلما كانت دقة الصورة وأبعادها أكبر، زادت سعتها التخزينية لاحتواء نصوص سرية أطول ومقالات كاملة.
|
||||
</li>
|
||||
<li className="rounded-2xl border border-slate-200 bg-slate-50/80 px-4 py-3">
|
||||
التلاعب اللوني لا يتجاوز 1/255 من قيمة كل لون، وهو تغيير مستحيل الإدراك بالعين البشرية المجردة.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[2rem] border border-white/70 bg-slate-950 p-6 text-white shadow-2xl shadow-slate-300/60">
|
||||
<h2 className="text-xl font-bold">نصيحة أمنية واحترافية</h2>
|
||||
<p className="mt-3 text-sm leading-7 text-slate-300">
|
||||
لتبادل الرسائل السرية بنجاح، أرسل الصورة النهائية كـ «ملف» (File / Document) في برامج المحادثة أو البريد
|
||||
الإلكتروني، لضمان عدم قيام تلك المنصات بضغط الصورة وإتلاف البيانات.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
src/index.css
Normal file
45
src/index.css
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* الأنماط العامة وإعدادات الخطوط (Global Styles & Typography) - SteganoPixel
|
||||
* =========================================================================
|
||||
* الجزء الأول من الشرح الهيكلي:
|
||||
* يوفر هذا الملف التهيئة الأساسية للمشروع بالاعتماد على إطار عمل Tailwind CSS (عبر التوجيه @import "tailwindcss").
|
||||
* نقوم بضبط المتغيرات الجذرية (:root) لتهيئة بيئة العرض التلقائية، وتحديد عائلة الخطوط الافتراضية (Inter و System-UI)
|
||||
* لضمان عرض النصوص العربية والإنجليزية بأبهى حلة وبأقصى درجات الوضوح على جميع أنظمة التشغيل (Windows, macOS, iOS, Android).
|
||||
*
|
||||
* الجزء الثاني من الشرح الهيكلي:
|
||||
* يعمل هذا الملف كطبقة تنسيق أساسية تمنع التفاوت البصري بين المتصفحات (Cross-Browser Normalization). نقوم بتصفير
|
||||
* الهوامش الافتراضية (margin: 0)، وضبط السلوك التفاعلي للتمرير (scroll-behavior)، وتحديد الحد الأدنى لعرض الشاشة (320px)
|
||||
* لضمان عدم انكسار التخطيط الشبكي (Grid/Flex) على الهواتف الذكية القديمة أو الشاشات الضيقة، مع دعم العرض التلقائي للصور.
|
||||
*/
|
||||
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
24
src/main.tsx
Normal file
24
src/main.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* نقطة الإطلاق الجذرية (Application Bootstrapper) - SteganoPixel
|
||||
* =================================================================
|
||||
* الجزء الأول من الشرح الهيكلي:
|
||||
* يقوم هذا الملف بدور المشغل الأساسي (Bootstrapper) لتطبيق React. يعتمد على أحدث واجهات البرمجة في React 18+ (`createRoot`)
|
||||
* لتفعيل آلية التصيير المتزامن (Concurrent Rendering). كما يغلف شجرة المكونات بالكامل داخل مكون `<StrictMode>` الذي
|
||||
* يساعد المطورين خلال بيئة التطوير على اكتشاف أي مشاكل خفية في دورة حياة المكونات، أو استخدام أدوات قديمة، أو آثار جانبية غير مرغوبة.
|
||||
*
|
||||
* الجزء الثاني من الشرح الهيكلي:
|
||||
* يعمل هذا الملف كجسر يربط بين المستند الفعلي (`index.html`) ومنطق التطبيق المرئي (`App.tsx`). يقوم باستيراد ملف الأنماط
|
||||
* الأساسي (`index.css`) لضمان حقن قواعد Tailwind CSS الجذرية قبل تصيير أي عنصر. إذا تعذر العثور على عنصر `#root` في
|
||||
* المستند، سيتوقف المحرك فوراً، ما يمثل آلية حماية ضد تشغيل التطبيق في بيئة HTML غير متوافقة.
|
||||
*/
|
||||
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
المرجع في مشكلة جديدة
حظر مستخدم