// Common UI components for the Duyuf app
// ─── Icons ────────────────────────────────────────────────────────────
function Icon({ name, size = 22, color = 'currentColor', strokeWidth = 1.7 }) {
const s = { width: size, height: size, fill: 'none', stroke: color, strokeWidth, strokeLinecap: 'round', strokeLinejoin: 'round' };
switch (name) {
case 'home': return ( );
case 'explore': return ( );
case 'briefcase': return ( );
case 'wallet': return ( );
case 'user': return ( );
case 'plane': return ( );
case 'bus': return ( );
case 'bed': return ( );
case 'car': return ( );
case 'kaaba': return ( );
case 'search': return ( );
case 'bell': return ( );
case 'heart': return ( );
case 'star': return ( );
case 'star-fill': return ( );
case 'arrow-l': return ( );
case 'arrow-r': return ( );
case 'arrow-down': return ( );
case 'pin': return ( );
case 'calendar': return ( );
case 'plus': return ( );
case 'minus': return ( );
case 'check': return ( );
case 'close': return ( );
case 'filter': return ( );
case 'qr': return ( );
case 'gift': return ( );
case 'shield': return ( );
case 'wifi': return ( );
case 'coffee': return ( );
case 'settings': return ( );
case 'globe': return ( );
case 'moon': return ( );
case 'sun': return ( );
case 'chevron-r': return ( );
case 'chevron-l': return ( );
case 'phone': return ( );
case 'percent': return ( );
case 'mail': return ( );
case 'lock': return ( );
case 'logout': return ( );
case 'card': return ( );
case 'compass': return ( );
case 'dots': return ( );
case 'trend-up': return ( );
case 'scan': return ( );
default: return null;
}
}
// ─── Button ───────────────────────────────────────────────────────────
function Button({ children, kind = 'primary', size = 'md', icon, full, onClick, theme, style }) {
const T = theme;
const sizes = {
sm: { h: 36, px: 14, fs: 13, r: 10 },
md: { h: 48, px: 18, fs: 14.5, r: 14 },
lg: { h: 56, px: 22, fs: 16, r: 16 },
};
const sz = sizes[size];
const kinds = {
primary: { bg: T.primary, fg: '#fff', border: 'none', shadow: '0 6px 16px rgba(193,152,114,0.35)' },
secondary: { bg: T.surface, fg: T.text, border: `1px solid ${T.hairline}`, shadow: 'none' },
ghost: { bg: 'transparent', fg: T.primary, border: 'none', shadow: 'none' },
dark: { bg: T.ink, fg: T.cream, border: 'none', shadow: '0 6px 16px rgba(31,27,23,0.3)' },
soft: { bg: T.camelSoft, fg: T.camelDeep, border: 'none', shadow: 'none' },
};
const k = kinds[kind];
return (
{icon && }
{children}
);
}
// ─── Image placeholder ────────────────────────────────────────────────
function ImgSlot({ label, height = 160, radius = 16, tone = 'camel', children, theme }) {
const T = theme;
// Use brand tones for placeholders, with subtle pattern for hospitality feel
const tones = {
camel: { bg: T.camelSoft, fg: T.camelDeep, accent: T.camel },
brown: { bg: '#D8CFC4', fg: T.pastelBrown, accent: T.pastelBrown },
dark: { bg: T.inkSoft, fg: T.cream, accent: T.camel },
cream: { bg: T.creamDeep, fg: T.inkSoft, accent: T.camel },
};
const t = tones[tone] || tones.camel;
return (
{label && (
{label}
)}
{children}
);
}
// ─── Bottom tab bar (3 tabs: Home / Bookings / Profile) ────────────────
function BottomTabs({ active = 'home', theme, lang, onSelect }) {
const T = theme;
const l = L[lang];
const tabs = [
{ id: 'me', icon: 'user', label: l.profile },
{ id: 'home', icon: 'home', label: l.home },
{ id: 'trips', icon: 'briefcase', label: l.mybookings },
];
return (
{tabs.map(t => {
const isActive = t.id === active;
return (
onSelect && onSelect(t.id)} style={{
background: 'transparent', border: 0, cursor: 'pointer', padding: '6px 10px',
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
minWidth: 48,
}}>
{t.label}
);
})}
);
}
// ─── Status bar (iOS-style, themed) ───────────────────────────────────
function PhoneStatusBar({ theme, time = '9:41', overlay = false, color }) {
const T = theme;
const c = color || (T.isDark || overlay ? '#fff' : T.ink);
return (
{time}
{/* signal */}
{/* wifi */}
{/* battery */}
);
}
// ─── Home indicator ───────────────────────────────────────────────────
function HomeIndicator({ color = '#000' }) {
return (
);
}
// ─── Frame wrapper — replaces IOSDevice with our own to hold RTL etc. ─
function PhoneFrame({ width = 360, height = 760, theme, dir = 'ltr', children, statusOverlay = false, hideStatus = false }) {
const T = theme;
return (
{/* Dynamic island */}
{!hideStatus &&
}
{children}
);
}
// ─── Card ─────────────────────────────────────────────────────────────
function Card({ children, theme, padding = 16, radius = 18, style }) {
const T = theme;
return (
{children}
);
}
// ─── Section header ───────────────────────────────────────────────────
function SectionHead({ title, action, theme, style }) {
const T = theme;
return (
{title}
{action && (
{action}
)}
);
}
// ─── Star rating ──────────────────────────────────────────────────────
function Stars({ value = 4, color = '#C9882F', size = 12 }) {
return (
{[1,2,3,4,5].map(i => (
))}
);
}
// ─── Chip ─────────────────────────────────────────────────────────────
function Chip({ children, active, theme, icon, style, onClick }) {
const T = theme;
return (
{icon && }
{children}
);
}
// ─── Badge ────────────────────────────────────────────────────────────
function Badge({ children, tone = 'camel', theme, style }) {
const T = theme;
const tones = {
camel: { bg: T.camelSoft, fg: T.camelDeep },
green: { bg: 'rgba(63,142,92,0.14)', fg: '#2E6A45' },
warn: { bg: 'rgba(201,136,47,0.16)', fg: '#8C5D14' },
red: { bg: 'rgba(180,81,59,0.14)', fg: '#8A3A29' },
ink: { bg: T.ink, fg: T.cream },
};
const t = tones[tone] || tones.camel;
return (
{children}
);
}
// ─── Provider badge (marketplace — show vendor on every listing) ─────
function ProviderBadge({ theme, name, color, size = 'sm', verified = true, style }) {
const T = theme;
// Derive a brand chip from the provider name (deterministic by hash)
const palette = ['#0E5C3F', '#7F6D5F', '#1F8A5B', '#8C5D14', '#3C342D', '#5B7A8A', '#A87F5A'];
const hash = [...(name || 'X')].reduce((a, c) => a + c.charCodeAt(0), 0);
const bg = color || palette[hash % palette.length];
const initials = (name || 'P').split(' ').map(w => w[0]).slice(0, 2).join('');
const sizes = {
xs: { logo: 16, fs: 8, gap: 5, ts: 10 },
sm: { logo: 20, fs: 9, gap: 6, ts: 11 },
md: { logo: 26, fs: 10, gap: 8, ts: 12.5 },
};
const s = sizes[size];
return (
{initials}
{name}
{verified && (
)}
);
}
Object.assign(window, {
Icon, Button, ImgSlot, BottomTabs, PhoneStatusBar, HomeIndicator,
PhoneFrame, Card, SectionHead, Stars, Chip, Badge, ProviderBadge,
});