"use client"; // ============================================================ // LynkedUp Pro — shared design-system primitives. // Tokens live in dashboard.css (.dash-root). These components // only consume CSS variables so they theme automatically. // // Exports: Icon, Avatar, PageHead, Pill, Toggle, OtpField, // Modal, ToastProvider/useToast, Field, SegTabs, // StatusDot, Segmented. // ============================================================ import { createContext, useCallback, useContext, useEffect, useId, useRef, useState, type ReactNode, } from "react"; import { MessageCircle, Ticket, Phone, Mail, BookOpen, Rocket, Shield, ShieldCheck, Lock, CreditCard, User, Bell, Eye, EyeOff, Camera, Upload, Plus, Star, Send, Paperclip, Clock, MapPin, Monitor, Smartphone, Tablet, LogOut, ArrowRight, Info, Check, X, Search, ChevronDown, ChevronRight, Trash2, Globe, CheckCircle2, KeyRound, Pencil, Copy, RefreshCw, AlertTriangle, LayoutDashboard, Building2, FolderKanban, UserPlus, BadgeCheck, Filter, Truck, CloudLightning, Map as MapIcon, PenTool, Calculator, CalendarDays, Trophy, ListChecks, Users, Settings, Sparkles, MoreHorizontal, UsersRound, type LucideIcon, } from "lucide-react"; /* ---------------------------------------------------------- */ /* Icon — single named entry point used across the module */ /* ---------------------------------------------------------- */ const ICONS: Record = { chat: MessageCircle, ticket: Ticket, phone: Phone, mail: Mail, book: BookOpen, rocket: Rocket, shield: Shield, "shield-check": ShieldCheck, lock: Lock, card: CreditCard, user: User, bell: Bell, eye: Eye, "eye-off": EyeOff, camera: Camera, upload: Upload, plus: Plus, star: Star, send: Send, paperclip: Paperclip, clock: Clock, pin: MapPin, monitor: Monitor, mobile: Smartphone, tablet: Tablet, logout: LogOut, arrow: ArrowRight, info: Info, check: Check, x: X, search: Search, chevron: ChevronDown, "chevron-right": ChevronRight, trash: Trash2, globe: Globe, "check-circle": CheckCircle2, key: KeyRound, edit: Pencil, copy: Copy, refresh: RefreshCw, alert: AlertTriangle, privacy: ShieldCheck, devices: Monitor, desktop: Monitor, // sidebar / workspace nav dashboard: LayoutDashboard, owners: Building2, projects: FolderKanban, leads: UserPlus, verify: BadgeCheck, pipeline: Filter, dispatch: Truck, storm: CloudLightning, territory: MapIcon, procanvas: PenTool, estimates: Calculator, schedule: CalendarDays, leaderboard: Trophy, subtasks: ListChecks, people: Users, settings: Settings, ai: Sparkles, team: UsersRound, dots: MoreHorizontal, }; export function Icon({ name, size = 18, className, strokeWidth = 2 }: { name: string; size?: number; className?: string; strokeWidth?: number }) { const C = ICONS[name] ?? Info; return ; } /* ---------------------------------------------------------- */ /* Avatar */ /* ---------------------------------------------------------- */ export function Avatar({ initials, gradient = "linear-gradient(135deg,#fda913,#fd6d13)", size = 40, status, square = false, }: { initials: string; gradient?: string; size?: number; status?: "online" | "away" | "offline" | "busy"; square?: boolean }) { return ( {initials} {status && } ); } /* ---------------------------------------------------------- */ /* PageHead — section title block used at the top of each view */ /* ---------------------------------------------------------- */ export function PageHead({ eyebrow, title, subtitle, icon, actions }: { eyebrow?: string; title: string; subtitle?: string; icon?: string; actions?: ReactNode }) { return (
{icon && }
{eyebrow &&
{eyebrow}
}

{title}

{subtitle &&

{subtitle}

}
{actions &&
{actions}
}
); } /* ---------------------------------------------------------- */ /* Pill / StatusDot */ /* ---------------------------------------------------------- */ export function Pill({ children, tone = "muted", style }: { children: ReactNode; tone?: string; style?: React.CSSProperties }) { return {children}; } export function StatusDot({ status }: { status: "online" | "away" | "offline" | "busy" }) { return ; } /* ---------------------------------------------------------- */ /* Toggle */ /* ---------------------------------------------------------- */ export function Toggle({ checked, onChange, disabled, label }: { checked: boolean; onChange?: (v: boolean) => void; disabled?: boolean; label?: string }) { return ( ); } /* ---------------------------------------------------------- */ /* Field — labelled input wrapper */ /* ---------------------------------------------------------- */ export function Field({ label, hint, error, children, required }: { label: string; hint?: string; error?: string; children: ReactNode; required?: boolean }) { return ( ); } /* ---------------------------------------------------------- */ /* Segmented / SegTabs */ /* ---------------------------------------------------------- */ export function Segmented({ options, value, onChange }: { options: { value: string; label: string; icon?: string }[]; value: string; onChange: (v: string) => void }) { return (
{options.map((o) => ( ))}
); } export function SegTabs({ tabs, value, onChange }: { tabs: { value: string; label: string; icon?: string; badge?: number }[]; value: string; onChange: (v: string) => void }) { return (
{tabs.map((t) => ( ))}
); } /* ---------------------------------------------------------- */ /* OtpField — N-digit one-time-code input */ /* ---------------------------------------------------------- */ export function OtpField({ length = 6, value, onChange, autoFocus = true }: { length?: number; value: string; onChange: (v: string) => void; autoFocus?: boolean }) { const refs = useRef<(HTMLInputElement | null)[]>([]); useEffect(() => { if (autoFocus) refs.current[0]?.focus(); }, [autoFocus]); function setAt(i: number, char: string) { const next = value.split(""); next[i] = char; const joined = next.join("").slice(0, length); onChange(joined); } return (
{Array.from({ length }).map((_, i) => ( { refs.current[i] = el; }} inputMode="numeric" maxLength={1} className="ds-otp-box" value={value[i] ?? ""} onChange={(e) => { const d = e.target.value.replace(/\D/g, ""); if (!d) { setAt(i, ""); return; } // support paste of full code if (d.length > 1) { onChange((value.slice(0, i) + d).slice(0, length)); refs.current[Math.min(i + d.length, length - 1)]?.focus(); return; } setAt(i, d); refs.current[Math.min(i + 1, length - 1)]?.focus(); }} onKeyDown={(e) => { if (e.key === "Backspace" && !value[i] && i > 0) refs.current[i - 1]?.focus(); if (e.key === "ArrowLeft" && i > 0) refs.current[i - 1]?.focus(); if (e.key === "ArrowRight" && i < length - 1) refs.current[i + 1]?.focus(); }} /> ))}
); } /* ---------------------------------------------------------- */ /* Modal */ /* ---------------------------------------------------------- */ export function Modal({ open, onClose, title, subtitle, icon, children, footer, size = "md" }: { open: boolean; onClose: () => void; title: string; subtitle?: string; icon?: string; children: ReactNode; footer?: ReactNode; size?: "sm" | "md" | "lg"; }) { const titleId = useId(); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open, onClose]); if (!open) return null; return (
e.stopPropagation()}>
{icon && }

{title}

{subtitle &&

{subtitle}

}
{children}
{footer &&
{footer}
}
); } /* ---------------------------------------------------------- */ /* Toast */ /* ---------------------------------------------------------- */ type Toast = { id: number; tone: "success" | "info" | "error"; title: string; desc?: string }; type ToastCtx = { push: (t: Omit) => void }; const ToastContext = createContext(null); export function useToast() { const ctx = useContext(ToastContext); if (!ctx) return { push: () => {} }; return ctx; } let toastSeq = 1; export function ToastProvider({ children }: { children: ReactNode }) { const [items, setItems] = useState([]); const push = useCallback((t: Omit) => { const id = toastSeq++; setItems((s) => [...s, { ...t, id }]); setTimeout(() => setItems((s) => s.filter((x) => x.id !== id)), 3800); }, []); return ( {children}
{items.map((t) => (
{t.title}
{t.desc &&
{t.desc}
}
))}
); } /* ---------------------------------------------------------- */ /* Button — light helper so call sites stay terse */ /* ---------------------------------------------------------- */ export function Btn({ children, variant = "primary", icon, iconRight, onClick, disabled, type = "button", full, size = "md" }: { children: ReactNode; variant?: "primary" | "ghost" | "soft" | "danger" | "outline"; icon?: string; iconRight?: string; onClick?: () => void; disabled?: boolean; type?: "button" | "submit"; full?: boolean; size?: "sm" | "md"; }) { return ( ); }