// Sanad Design System — Shared Primitives
// Export to window for cross-file use
const sanadColors = {
// ليكسا بالأسود (2026-10-26): حبرٌ نجديّ للأسطح، رمليٌّ للنصّ، طينيٌّ للفعل
brand: '#c96442',
brandLight: '#e07a52',
brandDark: '#e8845f',
brandSubtle: 'rgba(224,122,82,0.16)',
bg: '#0f1715',
bgElevated: '#16211d',
bgSubtle: '#1d2a25',
bgDark: '#070b0a',
textPrimary: '#efe7d6',
textSecondary: '#c9c2b3',
textTertiary: '#a8a69e',
textMuted: '#8f9a94',
textInverted: '#efe7d6',
border: 'rgba(239,231,214,0.12)',
borderLight: 'rgba(239,231,214,0.08)',
error: '#ff8a8a',
focus: '#e07a52',
success: '#5fbf8a',
};
const sanadFonts = {
arabic: "'IBM Plex Sans Arabic', 'Noto Sans Arabic', sans-serif",
serif: "Georgia, serif",
mono: "'JetBrains Mono', monospace",
};
// ── Button ─────────────────────────────────────────────────
function SanadButton({ variant = 'primary', size = 'md', children, onClick, icon, disabled, style = {} }) {
const [hovered, setHovered] = React.useState(false);
const base = {
display: 'inline-flex', alignItems: 'center', gap: 7,
fontFamily: sanadFonts.arabic, fontWeight: 500, border: 'none',
cursor: disabled ? 'not-allowed' : 'pointer',
transition: 'all 180ms ease', borderRadius: 10,
opacity: disabled ? 0.45 : 1,
textDecoration: 'none',
};
const sizes = {
sm: { padding: '6px 12px', fontSize: 13, borderRadius: 8 },
md: { padding: '10px 18px', fontSize: 15 },
lg: { padding: '13px 26px', fontSize: 17, borderRadius: 12 },
};
const variants = {
primary: {
background: hovered ? sanadColors.brandDark : sanadColors.brand,
color: sanadColors.textInverted,
boxShadow: '0 0 0 1px rgba(239,231,214,0.10)',
},
secondary: {
background: hovered ? sanadColors.bgSubtle : sanadColors.bgElevated,
color: sanadColors.textPrimary,
boxShadow: '0 0 0 1px rgba(239,231,214,0.14)',
},
ghost: {
background: hovered ? 'rgba(0,0,0,0.06)' : 'transparent',
color: sanadColors.textSecondary,
boxShadow: 'none',
},
};
return (
);
}
// ── Badge ──────────────────────────────────────────────────
function SanadBadge({ children, color = 'brand' }) {
const colors = {
brand: { bg: sanadColors.brandSubtle, text: sanadColors.brand },
neutral: { bg: sanadColors.bgSubtle, text: sanadColors.textSecondary },
success: { bg: 'rgba(95,191,138,0.16)', text: sanadColors.success },
error: { bg: '#f7e5e5', text: sanadColors.error },
warning: { bg: '#fef3e2', text: '#a05e1a' },
info: { bg: '#deeefb', text: sanadColors.focus },
dark: { bg: sanadColors.bgDark, text: sanadColors.textInverted },
};
const c = colors[color] || colors.brand;
return (
{children}
);
}
// ── Input ──────────────────────────────────────────────────
function SanadInput({ placeholder, value, onChange, type = 'text', icon, multiline, rows = 3, style = {} }) {
const [focused, setFocused] = React.useState(false);
const baseStyle = {
width: '100%', padding: icon ? '10px 38px 10px 14px' : '10px 14px',
borderRadius: 8, border: 'none',
background: sanadColors.bgElevated,
boxShadow: focused
? `0 0 0 1px rgba(239,231,214,0.14), 0 0 0 3px rgba(56,152,236,0.25)`
: '0 0 0 1px rgba(239,231,214,0.14)',
fontFamily: sanadFonts.arabic, fontSize: 15, color: sanadColors.textPrimary,
outline: 'none', direction: 'rtl', resize: multiline ? 'vertical' : 'none',
transition: 'box-shadow 150ms ease', ...style,
};
const wrap = { position: 'relative', width: '100%' };
const iconStyle = { position: 'absolute', top: '50%', transform: 'translateY(-50%)', right: 12, color: sanadColors.textMuted, pointerEvents: 'none' };
if (multiline) return (
);
return (
{icon && {icon}}
setFocused(true)} onBlur={() => setFocused(false)} style={baseStyle} />
);
}
// ── Nav ────────────────────────────────────────────────────
function SanadNav({ activeView, onNavigate }) {
const links = [
{ id: 'home', label: 'الرئيسية' },
{ id: 'chat', label: 'استشارة قانونية' },
{ id: 'lawyers', label: 'المحامون' },
{ id: 'docs', label: 'تحليل مستند' },
];
return (
);
}
// ── Divider ────────────────────────────────────────────────
function SanadDivider({ style = {} }) {
return ;
}
// ── Card ───────────────────────────────────────────────────
function SanadCard({ children, style = {}, hover = false }) {
const [hovered, setHovered] = React.useState(false);
return (
hover && setHovered(true)}
onMouseLeave={() => hover && setHovered(false)}
style={{
background: sanadColors.bgElevated,
borderRadius: 14,
boxShadow: hovered
? '0 0 0 1px rgba(239,231,214,0.12), 0 4px 16px rgba(0,0,0,0.10)'
: '0 0 0 1px rgba(239,231,214,0.08), 0 2px 8px rgba(0,0,0,0.05)',
transition: 'box-shadow 180ms ease',
...style,
}}
>{children}
);
}
// Export all to window
/* ── شعار ليكسا: «ليكسا.» بالخطّ الكوفيّ ───────────────────────────────
tone: 'light' (على خلفيّةٍ فاتحة — حبر) | 'dark' (على خلفيّةٍ داكنة — رمل)
accent: لونُ النقطة — يتبعُ القسمَ الحاليّ (LEXA_SECTION_ACCENT) أو الطينيَّ افتراضيّاً */
const LEXA_SECTION_ACCENT = { home: '#c96442', assistant: '#c96442', search: '#1f8a8a', files: '#d49a1e', pricing: '#7c5cff', learning: '#3478f6', admin: '#5e5d59' };
function LexaMark({ size = 30, tone = 'light', accent, onClick, style = {} }) {
const ink = tone === 'dark' ? '#efe7d6' : '#141413';
return (
ليكسا.
);
}
/* ── شريطُ التحديثِ الحيّ: الأنظمةُ القادمةُ بعدّادِ سريانها + أحدثُ ما صدر (/api/laws/latest) ──
يلفُّ باستمرار (يتوقّفُ عند المرور)، والضغطُ على عنصرٍ يفتحُ الوثيقة. يتجدّدُ كلَّ 10 دقائق. */
const LX_TICK_CSS = `
.lxt{position:relative;display:flex;align-items:center;height:40px;background:#0a100e;border-top:1px solid rgba(239,231,214,.08);color:#c9c2b3;font-size:13px;overflow:hidden;direction:rtl;flex-shrink:0}
.lxt-lbl{position:relative;z-index:2;display:flex;align-items:center;gap:8px;height:100%;padding:0 14px;background:#0a100e;box-shadow:-18px 0 18px #0a100e;white-space:nowrap;font-weight:600;color:#efe7d6}
.lxt-dot{width:8px;height:8px;border-radius:50%;background:#5fbf8a;box-shadow:0 0 0 0 rgba(95,191,138,.6);animation:lxtPing 1.8s infinite}
@keyframes lxtPing{0%{box-shadow:0 0 0 0 rgba(95,191,138,.55)}70%{box-shadow:0 0 0 8px rgba(95,191,138,0)}100%{box-shadow:0 0 0 0 rgba(95,191,138,0)}}
.lxt-lbl small{font-weight:400;color:#8f9a94}
.lxt-win{flex:1;overflow:hidden;height:100%;-webkit-mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent);mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent)}
.lxt-track{display:flex;align-items:center;gap:26px;height:100%;width:max-content;animation:lxtRun var(--lxt-dur,80s) linear infinite}
.lxt:hover .lxt-track{animation-play-state:paused}
@keyframes lxtRun{from{transform:translateX(0)}to{transform:translateX(50%)}}
.lxt-i{display:inline-flex;align-items:center;gap:8px;white-space:nowrap;border:0;background:none;color:inherit;font:inherit;cursor:pointer;padding:0}
.lxt-i:hover b{color:#ffb08f}
.lxt-i b{color:#efe7d6;font-weight:600;transition:color .15s}
.lxt-pill{font-size:11.5px;font-weight:700;border-radius:20px;padding:2px 9px}
.lxt-pill.up{background:rgba(224,122,82,.16);color:#ffb08f}
.lxt-pill.soon{background:rgba(224,122,82,.9);color:#1d0f09;animation:lxtGlow 1.6s ease-in-out infinite}
@keyframes lxtGlow{50%{opacity:.65}}
.lxt-pill.new{background:rgba(95,191,138,.14);color:#8fd6ac}
.lxt-i small{color:#8f9a94;font-family:'JetBrains Mono',monospace;font-size:11.5px}
.lxt-sep{width:4px;height:4px;border-radius:50%;background:rgba(239,231,214,.2)}
@media (prefers-reduced-motion:reduce){.lxt-track{animation:none}.lxt-dot,.lxt-pill.soon{animation:none}}
@media (max-width:640px){.lxt-lbl small{display:none}}
`;
function lxtAgo(s) {
if (!s) return '';
const d = new Date(String(s).replace(' ', 'T') + ':00');
const h = Math.max(0, Math.round((Date.now() - d.getTime()) / 3600000));
if (h < 1) return 'منذ دقائق';
if (h < 24) return 'منذ ' + h + ' ساعة';
const days = Math.round(h / 24);
return days === 1 ? 'أمس' : 'منذ ' + days + ' أيام';
}
function lxtCount(days) {
if (days <= 0) return 'دخل حيّز النفاذ';
if (days === 1) return 'يسري غداً';
if (days === 2) return 'يسري بعد يومين';
if (days <= 10) return 'يسري بعد ' + days + ' أيام';
return 'يسري بعد ' + days + ' يوماً';
}
function LxUpdateTicker({ style }) {
const [d, setD] = React.useState(null);
React.useEffect(() => {
let alive = true;
const load = () => fetch(((window.api && window.api.baseUrl) || '') + '/api/laws/latest?n=14')
.then(r => (r.ok ? r.json() : null)).then(x => { if (alive && x) setD(x); }).catch(() => {});
load();
const t = setInterval(load, 10 * 60 * 1000);
return () => { alive = false; clearInterval(t); };
}, []);
if (!d) return null;
const open = (did) => { location.href = location.pathname + '?doc=' + encodeURIComponent(did); };
const items = [
...d.upcoming.filter(u => u.days > -3).map(u => (
)),
...d.latest.map(x => (
)),
];
const withSep = items.flatMap((it, i) => [it, ]);
return (
تحديثٌ حيّ · آخر تحديث للقاعدة {lxtAgo(d.updated_at)}
{withSep}{withSep.map((el, i) => React.cloneElement(el, { key: 'b' + i }))}
);
}
Object.assign(window, { LexaMark, LEXA_SECTION_ACCENT, LxUpdateTicker,
SanadButton, SanadBadge, SanadInput, SanadNav, SanadDivider, SanadCard,
sanadColors, sanadFonts,
});