fix(leads): portaled dropdowns, flex-wrapper icons, responsive layout, and UI polish

- Replace all native <select> with LeadCustomSelect (ReactDOM.createPortal into body,
  getBoundingClientRect positioning, e.composedPath() scroll detection)
- Add LeadCombobox: fuzzy-search state picker (searches abbr + full name, portal, onMouseDown)
- Convert Phone/MapPin icons from absolute-positioned to flex-wrapper siblings of bg-transparent input
- Fix dark mode invisible text: all inputs converted from isDark JS conditionals to dark: variants
- Fix urgency Standard pill visibility: container bg-zinc-900, selected bg-zinc-700 ring-1 shadow-lg
- Fix City/State/ZIP mobile overlap: flex -> grid-cols-2 (mobile) / grid-cols-[1fr_5.5rem_6rem] (sm+)
- Fix urgency section hidden behind mobile sticky footer: pb-28 -> pb-44 md:pb-10
- Replace all compact type pickers (phone/email) with LeadCustomSelect compact mode
- Update README.md with full lead creation architecture docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Satyam
2026-03-13 17:22:58 +05:30
parent 205ada99e9
commit d3e2944fa3
10 changed files with 606 additions and 270 deletions
+17 -52
View File
@@ -1,12 +1,11 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { ChevronDown } from 'lucide-react';
import { useTheme } from '../../context/ThemeContext';
import { LEAD_FORM_OPTIONS } from '../../data/mockStore';
import UrgencyPillSelector from './UrgencyPillSelector';
import LeadCustomSelect from './LeadCustomSelect';
// ---------------------------------------------------------------------------
// Shared field primitives (local to Job section for now)
// Shared field primitives (local to Job section)
// ---------------------------------------------------------------------------
function FieldLabel({ children }) {
return (
@@ -16,44 +15,7 @@ function FieldLabel({ children }) {
);
}
function LeadSelect({ label, value, onChange, options, placeholder }) {
const { theme } = useTheme();
const isDark = theme === 'dark';
return (
<div>
<FieldLabel>{label}</FieldLabel>
<div className="relative">
<select
value={value}
onChange={e => onChange(e.target.value)}
className={`w-full appearance-none rounded-xl px-4 py-3 pr-10 text-sm font-medium cursor-pointer
outline-none transition-all duration-150
${isDark
? 'bg-zinc-800/60 border border-zinc-700 text-white focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20'
: 'bg-white border border-zinc-200 text-zinc-800 focus:border-blue-400 focus:ring-2 focus:ring-blue-100'
}
${!value ? (isDark ? 'text-zinc-500' : 'text-zinc-400') : ''}
`}
>
<option value="" disabled>{placeholder || 'Select…'}</option>
{options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<ChevronDown
size={14}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-400 pointer-events-none"
/>
</div>
</div>
);
}
function LeadTextarea({ label, value, onChange, placeholder, rows = 3 }) {
const { theme } = useTheme();
const isDark = theme === 'dark';
return (
<div>
<FieldLabel>{label}</FieldLabel>
@@ -62,13 +24,14 @@ function LeadTextarea({ label, value, onChange, placeholder, rows = 3 }) {
onChange={e => onChange(e.target.value)}
placeholder={placeholder}
rows={rows}
className={`w-full rounded-xl px-4 py-3 text-sm font-medium resize-none
className="w-full rounded-xl px-4 py-3 text-sm font-medium resize-none
outline-none transition-all duration-150
${isDark
? 'bg-zinc-800/60 border border-zinc-700 text-white placeholder-zinc-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20'
: 'bg-white border border-zinc-200 text-zinc-800 placeholder-zinc-400 focus:border-blue-400 focus:ring-2 focus:ring-blue-100'
}
`}
bg-white dark:bg-zinc-800/60
border border-zinc-200 dark:border-zinc-700
text-zinc-800 dark:text-white
placeholder-zinc-400 dark:placeholder-zinc-500
focus:border-blue-400 dark:focus:border-blue-500
focus:ring-2 focus:ring-blue-100 dark:focus:ring-blue-500/20"
/>
</div>
);
@@ -87,16 +50,15 @@ const collapseTransition = {
// ---------------------------------------------------------------------------
export default function LeadJobSection({ formData, updateField, isQuickCapture }) {
return (
// No gap/space-y — each block controls its own top padding to avoid
// phantom gaps from collapsed motion.divs
<div>
{/* --- Lead Source — always visible --- */}
<LeadSelect
<LeadCustomSelect
label="Lead Source"
value={formData.leadSource}
onChange={v => updateField('leadSource', v)}
options={LEAD_FORM_OPTIONS.leadSources}
placeholder="How did you find this lead?"
accent="blue"
/>
{/* --- Lead Type — Full Form only --- */}
@@ -111,12 +73,13 @@ export default function LeadJobSection({ formData, updateField, isQuickCapture }
className="overflow-hidden"
>
<div className="pt-4">
<LeadSelect
<LeadCustomSelect
label="Lead Type"
value={formData.leadType}
onChange={v => updateField('leadType', v)}
options={LEAD_FORM_OPTIONS.leadTypes}
placeholder="Residential, Commercial…"
accent="blue"
/>
</div>
</motion.div>
@@ -135,19 +98,21 @@ export default function LeadJobSection({ formData, updateField, isQuickCapture }
className="overflow-hidden"
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-4">
<LeadSelect
<LeadCustomSelect
label="Work Type"
value={formData.workType}
onChange={v => updateField('workType', v)}
options={LEAD_FORM_OPTIONS.workTypes}
placeholder="Roof Replacement, Repair…"
accent="blue"
/>
<LeadSelect
<LeadCustomSelect
label="Trade Type"
value={formData.tradeType}
onChange={v => updateField('tradeType', v)}
options={LEAD_FORM_OPTIONS.tradeTypes}
placeholder="Roofing, Gutter, Siding…"
accent="blue"
/>
</div>
</motion.div>