// =========================================
// DXA Admin · shared UI atoms + icons
// =========================================
const { useRef: _useRef, useState: _useState } = React;
const useRef = _useRef;
const useState = _useState;
const cls = (...a) => a.filter(Boolean).join(' ');
// Inline SVG icon set (24x24 viewBox, currentColor strokes)
const sv = (d, opts={}) => (
);
const I = {
dashboard: sv(<>>),
inbox: sv(<>>),
hero: sv(<>>),
about: sv(<>>),
services: sv(<>>),
stats: sv(<>>),
portfolio: sv(<>>),
clients: sv(<>>),
contact: sv(<>>),
footer: sv(<>>),
settings: sv(<>>),
theme: sv(<>>),
history: sv(<>>),
backup: sv(<>>),
plus: sv(<>>),
edit: sv(<>>),
trash: sv(<>>),
drag: sv(<>>),
check: sv(),
x: sv(<>>),
external: sv(<>>),
eye: sv(<>>),
reply: sv(<>>),
star: sv(),
image: sv(<>>),
video: sv(<>>),
upload: sv(<>>),
};
// === Pill ===
const Pill = ({ tone='muted', children }) => (
{children}
);
// === Field wrapper ===
const Field = ({ label, children }) => (
{children}
);
// === Bilingual text field (AR + EN stacked) ===
const Bilingual = ({ labelAr, labelEn, valueAr, valueEn, onChangeAr, onChangeEn, multiline=false, rows=2 }) => (
);
// =========================================
// Media upload system (high-quality images + videos)
// =========================================
// Format file size as human-readable
const fmtSize = (b) => {
if (!b && b !== 0) return '';
if (b < 1024) return b + ' B';
if (b < 1024*1024) return (b/1024).toFixed(1) + ' KB';
if (b < 1024*1024*1024) return (b/1024/1024).toFixed(1) + ' MB';
return (b/1024/1024/1024).toFixed(2) + ' GB';
};
const fmtDuration = (s) => {
if (!s && s !== 0) return '';
const m = Math.floor(s/60), sec = Math.floor(s%60);
return `${m}:${String(sec).padStart(2,'0')}`;
};
// Resolution quality badge
const qualityBadge = (w, h, isVideo) => {
const max = Math.max(w||0, h||0);
if (isVideo) {
if (max >= 3840) return { label:'4K · UHD', tone:'accent' };
if (max >= 2560) return { label:'2.5K · QHD', tone:'green' };
if (max >= 1920) return { label:'Full HD', tone:'green' };
if (max >= 1280) return { label:'HD', tone:'blue' };
return { label:'SD', tone:'amber' };
}
const mp = (w*h)/1_000_000;
if (mp >= 30) return { label:'فائق الدقة · ' + Math.round(mp) + 'MP', tone:'accent' };
if (mp >= 12) return { label:Math.round(mp) + 'MP · عالي', tone:'green' };
if (mp >= 4) return { label:Math.round(mp) + 'MP', tone:'blue' };
return { label:'منخفض · ' + mp.toFixed(1) + 'MP', tone:'amber' };
};
// Probe a File to get dimensions/duration before storing
const probeMedia = (file) => new Promise((resolve) => {
const isVideo = file.type.startsWith('video/');
const url = URL.createObjectURL(file);
if (isVideo) {
const v = document.createElement('video');
v.preload = 'metadata';
v.muted = true;
v.src = url;
v.onloadedmetadata = () => {
resolve({ src:url, name:file.name, size:file.size, type:file.type, width:v.videoWidth, height:v.videoHeight, duration:v.duration, isVideo:true });
};
v.onerror = () => resolve({ src:url, name:file.name, size:file.size, type:file.type, isVideo:true });
} else {
const img = new Image();
img.onload = () => {
resolve({ src:url, name:file.name, size:file.size, type:file.type, width:img.naturalWidth, height:img.naturalHeight, isVideo:false });
};
img.onerror = () => resolve({ src:url, name:file.name, size:file.size, type:file.type, isVideo:false });
img.src = url;
}
});
// === MediaSlot: a full upload + preview slot ===
const MediaSlot = ({ value, onChange, kind='photo', aspect, label, hint }) => {
const fileInputRef = useRef(null);
const [over, setOver] = useState(false);
const [busy, setBusy] = useState(false);
const accept = kind === 'video'
? 'video/mp4,video/quicktime,video/webm,video/x-matroska'
: kind === 'any'
? 'image/*,video/*'
: 'image/jpeg,image/png,image/webp,image/avif';
const handleFiles = async (files) => {
const file = files && files[0];
if (!file) return;
setBusy(true);
const meta = await probeMedia(file);
onChange(meta);
setBusy(false);
};
const onDrop = (e) => {
e.preventDefault(); setOver(false);
handleFiles(e.dataTransfer.files);
};
const onPick = (e) => handleFiles(e.target.files);
const ar = aspect || (kind==='video' ? '16 / 9' : '4 / 3');
const hasMedia = !!(value && value.src);
const isVid = hasMedia && value.isVideo;
const qb = hasMedia && value.width ? qualityBadge(value.width, value.height, isVid) : null;
return (
{e.preventDefault(); setOver(true);}}
onDragLeave={()=>setOver(false)}
onDrop={onDrop}
onClick={()=>!hasMedia && fileInputRef.current?.click()}
>
{hasMedia ? (
isVid ? (
) : (

)
) : (
{kind==='video' ? '▷' : '⌘'}
{kind==='video' ? 'اسحب الفيديو هنا' : 'اسحب الصورة هنا'}
أو انقر للاختيار من الجهاز
{hint &&
{hint}
}
)}
{busy &&
}
{hasMedia && (
)}
{qb &&
{qb.label}
}
{hasMedia && isVid && value.duration &&
{fmtDuration(value.duration)}
}
{hasMedia ? (
{value.name}
{fmtSize(value.size)}
{value.width && {value.width} × {value.height}}
{(value.type||'').split('/')[1]?.toUpperCase()}
) : (
{kind==='video'
? 'MP4 · MOV · WebM — حتى 500MB · يدعم 4K'
: 'JPG · PNG · WebP · AVIF — حتى 20MB · جودة أصلية'}
)}
);
};
// Backwards-compatible aliases
const ImgPreview = ({ kind='photo', label, value, onChange }) => (
{})} label={label}/>
);
const Dropzone = ({ value, onChange, kind='photo' }) => (
{})} />
);
// === Empty state ===
const EmptyState = ({ icon, title, hint, action }) => (
{icon}
{title}
{hint}
{action}
);
Object.assign(window, { cls, I, Pill, Field, Bilingual, MediaSlot, ImgPreview, Dropzone, EmptyState, fmtSize, fmtDuration, probeMedia });