// desktop-shared.jsx — sidebar, top bar, primitives for the desktop variant
// Relies on window.I (icons), window.ListingCard data helpers, etc. from mobile bundle.
const dskMonogram = (
);
// ─── SIDEBAR ────────────────────────────────────────────────────────────
function Sidebar({
active = 'kauf',
notifBadge = 0,
onNav = () => {},
user = null,
taskLimit = null,
onNewSearch = null,
}) {
const items = [
{ id: 'kauf', label: 'Kaufen', icon: },
{ id: 'miete', label: 'Mieten', icon: },
{ id: 'investor', label: 'Investor', icon: },
{ id: 'merk', label: 'Merkliste', icon: },
{ id: 'mitt', label: 'Mitteilungen', icon: , badge: notifBadge },
{ id: 'entd', label: 'Entdecken', icon: },
];
const admin = [
{ id: 'profile', label: 'Profil', icon: },
...(user?.isAdmin ? [{ id: 'jobs', label: 'Job-Monitor', icon: }] : []),
{ id: 'export', label: 'Exporte', icon: },
];
const quota = taskLimit?.remaining_evaluations == null ? 'Unbegrenzt' : taskLimit.remaining_evaluations;
return (
);
}
function desktopQuotaPct(taskLimit) {
const max = Number(taskLimit?.max_evaluations_per_day || 100);
const remaining = taskLimit?.remaining_evaluations == null ? max : Number(taskLimit.remaining_evaluations || 0);
if (!max) return 0;
return Math.max(0, Math.min(100, (remaining / max) * 100));
}
function SideLabel({ children }) {
return (
{children}
);
}
function NavItem({ icon, label, badge, active, onClick }) {
return (
);
}
// ─── TOP BAR ────────────────────────────────────────────────────────────
function DTopBar({
crumb,
title,
subtitle,
actions,
notifBadge = 0,
searchValue = '',
onSearchChange,
onSearchSubmit,
onNotifications,
searchPlaceholder = 'Adresse, Suchname oder Plattform-ID …',
showSearch = true,
compactActions = false,
}) {
const searchRef = React.useRef(null);
React.useEffect(() => {
if (!showSearch) return undefined;
const onKey = (event) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
event.preventDefault();
searchRef.current?.focus();
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [showSearch]);
const canSearch = showSearch && typeof onSearchChange === 'function';
const actionGap = compactActions ? 8 : 10;
const actionSize = compactActions ? 'sm' : 'md';
return (
{crumb && (
{crumb.map((c, i) => (
{c}
{i < crumb.length - 1 && }
))}
)}
{title}
{subtitle && (
{subtitle}
)}
{/* Search + actions */}
{showSearch && (
onSearchChange?.(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') onSearchSubmit?.(e.target.value);
}}
style={{
width: 220, border: 0, background: 'transparent', outline: 'none',
fontFamily: 'var(--font-sans)', fontSize: 13, color: 'var(--ink)',
minWidth: 0,
}}
/>
{searchValue ? (
) : (
⌘K
)}
)}
} badge={notifBadge} onClick={onNotifications} title="Mitteilungen" size={actionSize} />
{actions}
);
}
// ─── BUTTON ─────────────────────────────────────────────────────────────
function DButton({ kind = 'ghost', icon, children, onClick, badge, size = 'md', disabled = false, title }) {
const padding = size === 'sm' ? '7px 11px' : '9px 14px';
const styles = {
primary: { background: 'var(--ink)', color: '#fff', border: '1px solid var(--ink)' },
accent: { background: 'var(--primary)', color: '#fff', border: '1px solid var(--primary)' },
outline: { background: 'var(--surface)', color: 'var(--ink)', border: '1px solid var(--border-strong)' },
ghost: { background: 'var(--surface)', color: 'var(--ink-2)', border: '1px solid var(--border)' },
soft: { background: 'var(--surface-2)', color: 'var(--ink)', border: '1px solid transparent' },
icon: { background: 'transparent', color: 'var(--ink-2)', border: 'none', padding: 8 },
};
return (
);
}
// ─── STAT TILE (desktop) ────────────────────────────────────────────────
function DStat({ value, label, delta, icon, accent, trend }) {
return (
{label}
{icon && {React.cloneElement(icon, { width: 15, height: 15 })}}
{value}
{delta && (
{trend === 'up' && ▲}
{trend === 'down' && ▼}
{delta}
)}
);
}
// ─── SMALL UI HELPERS ──────────────────────────────────────────────────
function DChip({ children, tone = 'neutral', icon }) {
const tones = {
neutral: { bg: 'var(--surface-2)', c: 'var(--ink-2)' },
accent: { bg: 'var(--primary-soft)', c: 'var(--primary-ink)' },
success: { bg: 'var(--success-soft)', c: 'var(--success)' },
warn: { bg: 'var(--warn-soft)', c: 'var(--warn)' },
danger: { bg: 'var(--danger-soft)', c: 'var(--danger)' },
info: { bg: 'var(--info-soft)', c: 'var(--info)' },
ghost: { bg: 'transparent', c: 'var(--ink-muted)' },
};
const t = tones[tone];
return (
{icon && React.cloneElement(icon, { width: 12, height: 12 })}
{children}
);
}
function DSectionHead({ title, subtitle, right }) {
return (
{title}
{subtitle && (
{subtitle}
)}
{right}
);
}
function DCard({ children, padding = 16, style }) {
return (
{children}
);
}
// ─── SCORE PILL (desktop, smaller than mobile ring) ─────────────────────
function DScorePill({ score, size = 32 }) {
const color = score >= 80 ? '#2F8F5A' : score >= 60 ? '#C28428' : '#C8331C';
return (
{score}
);
}
// ─── DESKTOP APP WRAPPER ────────────────────────────────────────────────
function DesktopFrame({ children, url = 'app.immobot.de', tab = 'ImmoBot' }) {
return (
{children}
);
}
Object.assign(window, {
Sidebar, DTopBar, DButton, DStat, DChip, DSectionHead, DCard, DScorePill, DesktopFrame,
});