// Shared components for ResDri driver app.
// All RTL-aware. Theme via CSS vars.
// ────────────────────────────────────────────────────────────
// Iconography — single inline SVG factory. Solid for active, outline otherwise.
// ────────────────────────────────────────────────────────────
function Icon({ name, size = 22, color = 'currentColor', strokeWidth = 2 }) {
const props = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: color, strokeWidth, strokeLinecap: 'round', strokeLinejoin: 'round' };
const fillProps = { width: size, height: size, viewBox: '0 0 24 24', fill: color };
switch (name) {
case 'phone':
return ;
case 'message':
return ;
case 'chev-left':
return ;
case 'chev-right':
return ;
case 'chev-down':
return ;
case 'chev-up':
return ;
case 'close':
return ;
case 'check':
return ;
case 'check-circle':
return ;
case 'list':
return ;
case 'map':
return ;
case 'nav':
return ;
case 'walk':
return ;
case 'shop':
return ;
case 'package':
return ;
case 'food':
return ;
case 'cart':
return ;
case 'bike':
return ;
case 'wallet':
return ;
case 'shekel':
return ;
case 'whatsapp':
return ;
case 'lock':
return ;
case 'pin':
return ;
case 'camera':
return ;
case 'volume':
return ;
case 'clock':
return ;
case 'arrow-right':
return ;
case 'arrow-left':
return ;
case 'gear':
return ;
case 'bell':
return ;
default:
return ;
}
}
// ────────────────────────────────────────────────────────────
// Vehicle icon (used in order cards + chips)
// ────────────────────────────────────────────────────────────
function VehicleIcon({ kind = 'car', size = 18, color = 'currentColor' }) {
if (kind === 'bike' || kind === 'motorbike') {
return ;
}
if (kind === 'bicycle') {
return ;
}
// car (default)
return ;
}
// ────────────────────────────────────────────────────────────
// Vertical icon — colored chip
// ────────────────────────────────────────────────────────────
function VerticalChip({ kind = 'food', size = 36 }) {
const colors = {
food: { bg: '#FEE2E2', fg: '#E11D48' },
parcel: { bg: '#DBEAFE', fg: '#2563EB' },
grocery: { bg: '#DCFCE7', fg: '#16A34A' },
};
const c = colors[kind];
const iconName = { food: 'food', parcel: 'package', grocery: 'cart' }[kind];
return (
);
}
// ────────────────────────────────────────────────────────────
// Big Number — used for payout / cash / etc. 3 sizes.
// Always LTR, tabular, with ₪ glyph in proper position.
// ────────────────────────────────────────────────────────────
function BigNumber({ value, size = 'md', currency = '₪', accent = false, color, weight = 800 }) {
const sizes = {
sm: { num: 24, sym: 16, gap: 3 },
md: { num: 40, sym: 22, gap: 4 },
lg: { num: 64, sym: 32, gap: 6 },
xl: { num: 96, sym: 44, gap: 8 },
};
const s = sizes[size] || sizes.md;
const c = color || (accent ? 'var(--accent)' : 'var(--text)');
return (
{currency}
{value}
);
}
// ────────────────────────────────────────────────────────────
// Status pill
// ────────────────────────────────────────────────────────────
function StatusPill({ tone = 'neutral', children }) {
const tones = {
neutral: { bg: 'var(--surface-2)', fg: 'var(--text-2)', bd: 'var(--line)' },
success: { bg: 'color-mix(in oklch, var(--success) 14%, transparent)', fg: 'var(--success)', bd: 'color-mix(in oklch, var(--success) 30%, transparent)' },
warning: { bg: 'color-mix(in oklch, var(--warning) 14%, transparent)', fg: 'var(--warning)', bd: 'color-mix(in oklch, var(--warning) 30%, transparent)' },
danger: { bg: 'color-mix(in oklch, var(--danger) 14%, transparent)', fg: 'var(--danger)', bd: 'color-mix(in oklch, var(--danger) 30%, transparent)' },
primary: { bg: 'color-mix(in oklch, var(--primary) 14%, transparent)', fg: 'var(--primary)', bd: 'color-mix(in oklch, var(--primary) 30%, transparent)' },
accent: { bg: 'color-mix(in oklch, var(--accent) 18%, transparent)', fg: 'var(--accent-700)', bd: 'color-mix(in oklch, var(--accent) 40%, transparent)' },
};
const t = tones[tone] || tones.neutral;
return (
{children}
);
}
// ────────────────────────────────────────────────────────────
// Order card — new scannable, action-oriented variant.
// Layout (RTL):
// [VerticalIcon] [Business · pickup→dropoff with distance · time/efficiency] [payout BIG + bonus]
// [قبول inline button]
// Optional `featured` adds a teal highlight + "أفضل خيار" ribbon.
// ────────────────────────────────────────────────────────────
function OrderCard({ order, onClick, onAccept, compatible = true, featured = false }) {
const verticalLabel = { food: 'طعام', parcel: 'طرد', grocery: 'بقالة' }[order.kind];
const totalPay = order.payout + (order.bonus || 0);
const perKm = (totalPay / Math.max(0.5, order.distance)).toFixed(0);
return (
{/* Featured ribbon */}
{featured && (
أفضل خيار
)}
{/* HEADER ROW: business + payout (no third column) */}
{order.business}
{verticalLabel}
·
~{order.eta} د
·
{order.distance.toFixed(1)} كم
₪{perKm}/كم
{/* ROUTE — two stacked rows: pickup (from your location), dropoff (from restaurant) */}
{/* Pickup row */}
{order.pickup}
{order.toPickup?.toFixed(1) ?? '—'}
كم من موقعك
{/* Connector */}
{/* Dropoff row */}
{order.dropoff}
{order.toDropoff?.toFixed(1) ?? '—'}
كم من المتجر
{/* BOTTOM ROW: bonus + accept button */}
{order.bonus > 0 && (
+₪{order.bonus}
مكافأة
)}
{!compatible && غير متوافق}
{order.notes && (
ملاحظة
)}
);
}
// ────────────────────────────────────────────────────────────
// Action sheet — bottom anchored, used for active-order next-step.
// ────────────────────────────────────────────────────────────
function ActionSheet({ children, padding = 16 }) {
return (
);
}
// ────────────────────────────────────────────────────────────
// Big primary action button — glove-friendly, 56dp+
// ────────────────────────────────────────────────────────────
function PrimaryButton({ children, onClick, tone = 'primary', size = 'lg', disabled = false, leadingIcon, fullWidth = true }) {
const tones = {
primary: { bg: 'var(--primary)', fg: '#fff', sh: '0 6px 20px color-mix(in oklch, var(--primary) 28%, transparent)' },
success: { bg: 'var(--success)', fg: '#fff', sh: '0 6px 20px rgba(22,163,74,0.30)' },
danger: { bg: 'var(--danger)', fg: '#fff', sh: '0 6px 20px rgba(220,38,38,0.30)' },
accent: { bg: 'var(--accent)', fg: '#1A1300', sh: '0 6px 20px rgba(245,158,11,0.30)' },
ghost: { bg: 'var(--surface-2)', fg: 'var(--text)', sh: 'none', bd: '1px solid var(--line)' },
outline: { bg: 'transparent', fg: 'var(--text)', sh: 'none', bd: '1.5px solid var(--line-strong)' },
};
const t = tones[tone] || tones.primary;
const h = size === 'lg' ? 60 : size === 'md' ? 52 : 44;
return (
);
}
// ────────────────────────────────────────────────────────────
// "Go online" lever toggle — custom, big
// ────────────────────────────────────────────────────────────
function OnlineLever({ online, onToggle }) {
return (
);
}
// ────────────────────────────────────────────────────────────
// Slide-to-confirm — deliberate destructive/important action
// ────────────────────────────────────────────────────────────
function SlideToConfirm({ label = 'اسحب للتأكيد', onConfirm, color = 'var(--success)', icon = 'check' }) {
const trackRef = React.useRef(null);
const [x, setX] = React.useState(0);
const [done, setDone] = React.useState(false);
const draggingRef = React.useRef(false);
const startXRef = React.useRef(0);
const startPosRef = React.useRef(0);
const onStart = (e) => {
if (done) return;
draggingRef.current = true;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
startXRef.current = clientX;
startPosRef.current = x;
};
const onMove = (e) => {
if (!draggingRef.current || done) return;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const track = trackRef.current;
if (!track) return;
const trackW = track.clientWidth;
const maxX = trackW - 64; // thumb width + 8 padding
// RTL: thumb starts on the right (x=maxX initially via CSS), user drags it LEFT.
// We track displacement positively.
let delta = startXRef.current - clientX; // positive when moving left
let nx = startPosRef.current + delta;
nx = Math.max(0, Math.min(maxX, nx));
setX(nx);
if (nx >= maxX - 4) {
setDone(true);
draggingRef.current = false;
setTimeout(() => onConfirm && onConfirm(), 200);
}
};
const onEnd = () => {
if (!draggingRef.current) return;
draggingRef.current = false;
if (!done) setX(0);
};
React.useEffect(() => {
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onEnd);
window.addEventListener('touchmove', onMove, { passive: false });
window.addEventListener('touchend', onEnd);
return () => {
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onEnd);
window.removeEventListener('touchmove', onMove);
window.removeEventListener('touchend', onEnd);
};
});
// In RTL, "right" is the leading side. The thumb starts on the right and travels left.
return (
{/* Filled trail */}
{/* Label */}
{done ? 'تم' : label}
{/* Thumb — starts on the right (RTL leading), drags left */}
);
}
// ────────────────────────────────────────────────────────────
// Top "Today's earnings" summary bar (sits above map)
// ────────────────────────────────────────────────────────────
function EarningsHeader({ today = 142, orders = 4, rating = 4.8 }) {
return (
أرباح اليوم
طلبات
{orders}
التقييم
{rating}
★
);
}
// ────────────────────────────────────────────────────────────
// "Connecting" radar — used on incoming alert. Calm, dispatch-radio vibe.
// ────────────────────────────────────────────────────────────
function RadarPulse({ size = 180, color = 'var(--primary)' }) {
const wave = (delay) => (
);
return (
{[0, 0.6, 1.2].map(wave)}
);
}
Object.assign(window, {
Icon, VehicleIcon, VerticalChip,
BigNumber, StatusPill,
OrderCard, ActionSheet, PrimaryButton,
OnlineLever, SlideToConfirm,
EarningsHeader, RadarPulse,
});