2388 lines
66 KiB
Svelte
2388 lines
66 KiB
Svelte
<script lang="ts">
|
|
import { goto } from "$app/navigation";
|
|
import type { CWMember } from "$lib/optima-api/modules/cw";
|
|
|
|
interface CompanyContact {
|
|
firstName?: string;
|
|
lastName?: string;
|
|
cwId?: number;
|
|
inactive?: boolean;
|
|
title?: string;
|
|
phone?: string;
|
|
email?: string;
|
|
}
|
|
|
|
interface CompanyAddress {
|
|
line1?: string;
|
|
line2?: string | null;
|
|
city?: string;
|
|
state?: string;
|
|
zip?: string;
|
|
country?: string;
|
|
}
|
|
|
|
export let isOpen = false;
|
|
export let onSuccess: () => void = () => {};
|
|
|
|
// ── Form state ──
|
|
let name = "";
|
|
let expectedCloseDate = "";
|
|
let description = "";
|
|
let source = "";
|
|
let customerPO = "";
|
|
let primarySalesRepId = "";
|
|
let secondarySalesRepId = "";
|
|
let companyId = "";
|
|
let companyName = "";
|
|
let companyOptimaId = "";
|
|
|
|
// ── Contact & Site state ──
|
|
let contacts: CompanyContact[] = [];
|
|
let selectedContactId = "";
|
|
let companyAddress: CompanyAddress | null = null;
|
|
let isLoadingCompanyDetails = false;
|
|
|
|
// ── UI state ──
|
|
let isSubmitting = false;
|
|
let submitError = "";
|
|
let currentStep = 0; // 0 = details, 1 = assignment, 2 = contact & site, 3 = review
|
|
let members: CWMember[] = [];
|
|
let isLoadingMembers = false;
|
|
let companies: { id: number; optimaId: string; name: string }[] = [];
|
|
let isSearchingCompanies = false;
|
|
let companySearchInput = "";
|
|
let companySearchTimer: ReturnType<typeof setTimeout>;
|
|
let showCompanyDropdown = false;
|
|
let navigateOnCreate = true;
|
|
let companyInputWrapEl: HTMLDivElement;
|
|
let dropdownStyle = "";
|
|
|
|
const steps = [
|
|
{ label: "Details", icon: "document" },
|
|
{ label: "Assignment", icon: "people" },
|
|
{ label: "Contact & Site", icon: "contact" },
|
|
{ label: "Review", icon: "check" },
|
|
];
|
|
|
|
// ── Validation ──
|
|
$: isStep0Valid = name.trim().length > 0 && expectedCloseDate.length > 0;
|
|
$: isStep1Valid = primarySalesRepId.length > 0 && companyId.length > 0;
|
|
$: isStep2Valid = true; // Contact & Site step is optional
|
|
$: isValid = isStep0Valid && isStep1Valid && isStep2Valid;
|
|
|
|
$: selectedPrimaryRep = members.find(
|
|
(m) => String(m.id) === primarySalesRepId,
|
|
);
|
|
$: selectedSecondaryRep = members.find(
|
|
(m) => String(m.id) === secondarySalesRepId,
|
|
);
|
|
$: selectedContact = contacts.find(
|
|
(c) => String(c.cwId) === selectedContactId,
|
|
);
|
|
$: activeContacts = contacts.filter((c) => !c.inactive);
|
|
|
|
// ── Default close date to 30 days from now ──
|
|
$: if (isOpen && !expectedCloseDate) {
|
|
const d = new Date();
|
|
d.setDate(d.getDate() + 30);
|
|
expectedCloseDate = d.toISOString().split("T")[0];
|
|
}
|
|
|
|
// ── Load CW members for sales rep dropdowns ──
|
|
async function loadMembers() {
|
|
if (members.length > 0) return;
|
|
isLoadingMembers = true;
|
|
try {
|
|
const res = await fetch("/api/cw/members");
|
|
const json = await res.json();
|
|
members = json.data ?? [];
|
|
// Auto-select first member as primary sales rep if not already set
|
|
if (!primarySalesRepId && members.length > 0) {
|
|
primarySalesRepId = String(members[0].id);
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to load members:", err);
|
|
} finally {
|
|
isLoadingMembers = false;
|
|
}
|
|
}
|
|
|
|
// ── Company search ──
|
|
function updateDropdownPosition() {
|
|
if (!companyInputWrapEl) return;
|
|
const rect = companyInputWrapEl.getBoundingClientRect();
|
|
dropdownStyle = `position:fixed; top:${rect.bottom + 4}px; left:${rect.left}px; width:${rect.width}px;`;
|
|
}
|
|
|
|
function handleCompanySearch() {
|
|
clearTimeout(companySearchTimer);
|
|
showCompanyDropdown = true;
|
|
if (!companySearchInput.trim()) {
|
|
companies = [];
|
|
return;
|
|
}
|
|
isSearchingCompanies = true;
|
|
companySearchTimer = setTimeout(async () => {
|
|
try {
|
|
const res = await fetch(
|
|
`/api/companies/search?search=${encodeURIComponent(companySearchInput)}&rpp=10`,
|
|
);
|
|
const json = await res.json();
|
|
companies = (json.data ?? []).map(
|
|
(c: { id: string; name: string; cw_CompanyId?: number }) => ({
|
|
id: c.cw_CompanyId ?? Number(c.id),
|
|
optimaId: String(c.id),
|
|
name: c.name,
|
|
}),
|
|
);
|
|
requestAnimationFrame(updateDropdownPosition);
|
|
} catch {
|
|
companies = [];
|
|
} finally {
|
|
isSearchingCompanies = false;
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
function selectCompany(c: { id: number; optimaId: string; name: string }) {
|
|
companyId = String(c.id);
|
|
companyOptimaId = c.optimaId;
|
|
companyName = c.name;
|
|
companySearchInput = c.name;
|
|
showCompanyDropdown = false;
|
|
companies = [];
|
|
// Reset contact/site when company changes
|
|
contacts = [];
|
|
selectedContactId = "";
|
|
companyAddress = null;
|
|
}
|
|
|
|
function clearCompany() {
|
|
companyId = "";
|
|
companyOptimaId = "";
|
|
companyName = "";
|
|
companySearchInput = "";
|
|
companies = [];
|
|
showCompanyDropdown = false;
|
|
// Clear contact/site data
|
|
contacts = [];
|
|
selectedContactId = "";
|
|
companyAddress = null;
|
|
}
|
|
|
|
// ── Load company details (contacts + address) ──
|
|
async function loadCompanyDetails() {
|
|
if (!companyOptimaId) return;
|
|
isLoadingCompanyDetails = true;
|
|
try {
|
|
const res = await fetch(`/api/companies/${companyOptimaId}/details`);
|
|
const json = await res.json();
|
|
const data = json.data;
|
|
if (data) {
|
|
const allContacts: CompanyContact[] = data.cw_Data?.allContacts ?? [];
|
|
contacts = allContacts;
|
|
companyAddress = data.cw_Data?.address ?? null;
|
|
|
|
// Auto-select first active contact as default
|
|
const active = allContacts.filter((c: CompanyContact) => !c.inactive);
|
|
if (active.length > 0 && active[0].cwId) {
|
|
selectedContactId = String(active[0].cwId);
|
|
} else if (allContacts.length > 0 && allContacts[0].cwId) {
|
|
selectedContactId = String(allContacts[0].cwId);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to load company details:", err);
|
|
} finally {
|
|
isLoadingCompanyDetails = false;
|
|
}
|
|
}
|
|
|
|
// ── Step navigation ──
|
|
function nextStep() {
|
|
if (currentStep === 0 && !isStep0Valid) return;
|
|
if (currentStep === 1 && !isStep1Valid) return;
|
|
if (currentStep < 3) {
|
|
currentStep++;
|
|
if (currentStep === 1) loadMembers();
|
|
if (currentStep === 2) loadCompanyDetails();
|
|
}
|
|
}
|
|
|
|
function prevStep() {
|
|
if (currentStep > 0) currentStep--;
|
|
}
|
|
|
|
function goToStep(step: number) {
|
|
if (step === 0) {
|
|
currentStep = 0;
|
|
return;
|
|
}
|
|
if (step === 1 && isStep0Valid) {
|
|
currentStep = 1;
|
|
loadMembers();
|
|
return;
|
|
}
|
|
if (step === 2 && isStep0Valid && isStep1Valid) {
|
|
currentStep = 2;
|
|
loadCompanyDetails();
|
|
return;
|
|
}
|
|
if (step === 3 && isStep0Valid && isStep1Valid && isStep2Valid) {
|
|
currentStep = 3;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// ── Submit ──
|
|
async function handleSubmit() {
|
|
if (!isValid) return;
|
|
isSubmitting = true;
|
|
submitError = "";
|
|
|
|
try {
|
|
const body: Record<string, unknown> = {
|
|
name: name.trim(),
|
|
expectedCloseDate,
|
|
};
|
|
if (description.trim()) body.notes = description.trim();
|
|
if (source.trim()) body.source = source.trim();
|
|
if (customerPO.trim()) body.customerPO = customerPO.trim();
|
|
if (primarySalesRepId)
|
|
body.primarySalesRep = { id: Number(primarySalesRepId) };
|
|
if (secondarySalesRepId)
|
|
body.secondarySalesRep = { id: Number(secondarySalesRepId) };
|
|
if (companyId) body.company = { id: Number(companyId) };
|
|
if (selectedContactId) body.contact = { id: Number(selectedContactId) };
|
|
|
|
const res = await fetch("/api/sales/opportunities", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const errData = await res.json().catch(() => ({}));
|
|
throw new Error(
|
|
errData.message || `Failed to create opportunity (${res.status})`,
|
|
);
|
|
}
|
|
|
|
const result = await res.json();
|
|
const newId = result?.data?.id;
|
|
const shouldNavigate = navigateOnCreate && !!newId;
|
|
|
|
if (!newId) {
|
|
console.warn("No opportunity ID in response:", JSON.stringify(result));
|
|
}
|
|
|
|
reset();
|
|
|
|
if (shouldNavigate) {
|
|
await goto(`/sales/opportunity/${newId}`);
|
|
} else {
|
|
onSuccess();
|
|
}
|
|
} catch (err) {
|
|
submitError =
|
|
err instanceof Error ? err.message : "Failed to create opportunity";
|
|
console.error("Failed to create opportunity:", err);
|
|
} finally {
|
|
isSubmitting = false;
|
|
}
|
|
}
|
|
|
|
// ── Reset ──
|
|
function reset() {
|
|
name = "";
|
|
expectedCloseDate = "";
|
|
description = "";
|
|
source = "";
|
|
customerPO = "";
|
|
primarySalesRepId = "";
|
|
secondarySalesRepId = "";
|
|
companyId = "";
|
|
companyOptimaId = "";
|
|
companyName = "";
|
|
companySearchInput = "";
|
|
companies = [];
|
|
showCompanyDropdown = false;
|
|
contacts = [];
|
|
selectedContactId = "";
|
|
companyAddress = null;
|
|
isLoadingCompanyDetails = false;
|
|
isSubmitting = false;
|
|
submitError = "";
|
|
currentStep = 0;
|
|
navigateOnCreate = true;
|
|
}
|
|
|
|
function handleClose() {
|
|
reset();
|
|
isOpen = false;
|
|
}
|
|
|
|
function handleBackdropClick() {
|
|
handleClose();
|
|
}
|
|
|
|
function handleBackdropKeydown(e: KeyboardEvent) {
|
|
if (e.key === "Escape") handleClose();
|
|
}
|
|
</script>
|
|
|
|
{#if isOpen}
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="co-modal-backdrop"
|
|
on:click={handleBackdropClick}
|
|
on:keydown={handleBackdropKeydown}
|
|
>
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<div
|
|
class="co-modal"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label="Create Opportunity"
|
|
tabindex="-1"
|
|
on:click|stopPropagation
|
|
>
|
|
<!-- ── Header ── -->
|
|
<div class="co-modal-header">
|
|
<div class="co-modal-title-group">
|
|
<div class="co-modal-icon">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.5"
|
|
width="22"
|
|
height="22"
|
|
>
|
|
<path
|
|
d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h2>Create Opportunity</h2>
|
|
<p class="co-modal-subtitle">Set up a new sales opportunity</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
class="co-close-btn"
|
|
on:click={handleClose}
|
|
type="button"
|
|
aria-label="Close modal"
|
|
>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="18"
|
|
height="18"
|
|
>
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ── Step indicator ── -->
|
|
<div class="co-steps">
|
|
{#each steps as step, i}
|
|
<button
|
|
class="co-step"
|
|
class:active={i === currentStep}
|
|
class:completed={i < currentStep}
|
|
class:disabled={(i > 0 && !isStep0Valid) ||
|
|
(i > 1 && !isStep1Valid) ||
|
|
(i > 2 && !isStep2Valid)}
|
|
on:click={() => goToStep(i)}
|
|
disabled={(i > 0 && !isStep0Valid) ||
|
|
(i > 1 && !isStep1Valid) ||
|
|
(i > 2 && !isStep2Valid)}
|
|
>
|
|
<span class="co-step-number" class:check={i < currentStep}>
|
|
{#if i < currentStep}
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
width="12"
|
|
height="12"
|
|
>
|
|
<polyline points="20 6 9 17 4 12" />
|
|
</svg>
|
|
{:else}
|
|
{i + 1}
|
|
{/if}
|
|
</span>
|
|
<span class="co-step-label">{step.label}</span>
|
|
</button>
|
|
{#if i < steps.length - 1}
|
|
<div class="co-step-connector" class:filled={i < currentStep}></div>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
|
|
<!-- ── Body ── -->
|
|
<div class="co-modal-body">
|
|
{#if submitError}
|
|
<div class="co-error-banner">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="16"
|
|
height="16"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<line x1="12" y1="8" x2="12" y2="12" />
|
|
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
</svg>
|
|
<span>{submitError}</span>
|
|
<button
|
|
class="co-error-dismiss"
|
|
on:click={() => (submitError = "")}
|
|
aria-label="Dismiss error"
|
|
>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="14"
|
|
height="14"
|
|
>
|
|
<path d="M18 6L6 18M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Step 0: Details -->
|
|
{#if currentStep === 0}
|
|
<div class="co-step-content" style="animation: coFadeIn 0.2s ease">
|
|
<div class="co-section">
|
|
<h3 class="co-section-title">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="15"
|
|
height="15"
|
|
>
|
|
<path
|
|
d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"
|
|
/>
|
|
<polyline points="14 2 14 8 20 8" />
|
|
</svg>
|
|
Opportunity Details
|
|
</h3>
|
|
|
|
<div class="co-form-grid">
|
|
<div class="co-form-group co-full-width">
|
|
<label for="co-name">
|
|
Opportunity Name <span class="co-req">*</span>
|
|
</label>
|
|
<input
|
|
id="co-name"
|
|
type="text"
|
|
bind:value={name}
|
|
placeholder="e.g. Network Upgrade — Acme Corp"
|
|
disabled={isSubmitting}
|
|
class:has-value={name.trim().length > 0}
|
|
/>
|
|
</div>
|
|
|
|
<div class="co-form-group">
|
|
<label for="co-close-date">
|
|
Expected Close Date <span class="co-req">*</span>
|
|
</label>
|
|
<input
|
|
id="co-close-date"
|
|
type="date"
|
|
bind:value={expectedCloseDate}
|
|
disabled={isSubmitting}
|
|
class:has-value={expectedCloseDate.length > 0}
|
|
/>
|
|
</div>
|
|
|
|
<div class="co-form-group">
|
|
<label for="co-source">Source</label>
|
|
<input
|
|
id="co-source"
|
|
type="text"
|
|
bind:value={source}
|
|
placeholder="e.g. Referral, Website, Cold Call"
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
|
|
<div class="co-form-group">
|
|
<label for="co-po">Customer PO</label>
|
|
<input
|
|
id="co-po"
|
|
type="text"
|
|
bind:value={customerPO}
|
|
placeholder="PO number (optional)"
|
|
disabled={isSubmitting}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="co-section">
|
|
<h3 class="co-section-title">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="15"
|
|
height="15"
|
|
>
|
|
<path
|
|
d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"
|
|
/>
|
|
<path
|
|
d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"
|
|
/>
|
|
</svg>
|
|
Description
|
|
</h3>
|
|
<textarea
|
|
class="co-notes-textarea"
|
|
bind:value={description}
|
|
placeholder="Add a description for this opportunity…"
|
|
rows="3"
|
|
disabled={isSubmitting}
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 1: Assignment -->
|
|
{:else if currentStep === 1}
|
|
<div class="co-step-content" style="animation: coFadeIn 0.2s ease">
|
|
<div class="co-section">
|
|
<h3 class="co-section-title">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="15"
|
|
height="15"
|
|
>
|
|
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" />
|
|
<circle cx="9" cy="7" r="4" />
|
|
<path d="M23 21v-2a4 4 0 00-3-3.87" />
|
|
<path d="M16 3.13a4 4 0 010 7.75" />
|
|
</svg>
|
|
Sales Team
|
|
</h3>
|
|
|
|
<div class="co-form-grid">
|
|
<div class="co-form-group">
|
|
<label for="co-primary-rep"
|
|
>Primary Sales Rep <span class="co-req">*</span></label
|
|
>
|
|
{#if isLoadingMembers}
|
|
<div class="co-loading-field">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="14"
|
|
height="14"
|
|
class="co-spin-icon"
|
|
>
|
|
<circle
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
fill="none"
|
|
opacity="0.25"
|
|
/>
|
|
<path
|
|
d="M12 2a10 10 0 0 1 10 10"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
fill="none"
|
|
stroke-linecap="round"
|
|
/>
|
|
</svg>
|
|
Loading team members…
|
|
</div>
|
|
{:else}
|
|
<select
|
|
id="co-primary-rep"
|
|
bind:value={primarySalesRepId}
|
|
disabled={isSubmitting}
|
|
>
|
|
<option value="">— Select rep —</option>
|
|
{#each members as m}
|
|
<option value={String(m.id)}>{m.name}</option>
|
|
{/each}
|
|
</select>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="co-form-group">
|
|
<label for="co-secondary-rep">Secondary Sales Rep</label>
|
|
{#if isLoadingMembers}
|
|
<div class="co-loading-field">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="14"
|
|
height="14"
|
|
class="co-spin-icon"
|
|
>
|
|
<circle
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
fill="none"
|
|
opacity="0.25"
|
|
/>
|
|
<path
|
|
d="M12 2a10 10 0 0 1 10 10"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
fill="none"
|
|
stroke-linecap="round"
|
|
/>
|
|
</svg>
|
|
Loading team members…
|
|
</div>
|
|
{:else}
|
|
<select
|
|
id="co-secondary-rep"
|
|
bind:value={secondarySalesRepId}
|
|
disabled={isSubmitting}
|
|
>
|
|
<option value="">— None —</option>
|
|
{#each members.filter((m) => String(m.id) !== primarySalesRepId) as m}
|
|
<option value={String(m.id)}>{m.name}</option>
|
|
{/each}
|
|
</select>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="co-section">
|
|
<h3 class="co-section-title">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="15"
|
|
height="15"
|
|
>
|
|
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
|
|
<polyline points="9 22 9 12 15 12 15 22" />
|
|
</svg>
|
|
Company
|
|
</h3>
|
|
|
|
<div class="co-form-grid">
|
|
<div class="co-form-group co-full-width co-company-search-wrap">
|
|
<label for="co-company-search"
|
|
>Company <span class="co-req">*</span></label
|
|
>
|
|
{#if companyId}
|
|
<div class="co-selected-company">
|
|
<span class="co-company-chip">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="13"
|
|
height="13"
|
|
>
|
|
<path
|
|
d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"
|
|
/>
|
|
</svg>
|
|
{companyName}
|
|
</span>
|
|
<button
|
|
class="co-company-clear"
|
|
on:click={clearCompany}
|
|
type="button"
|
|
aria-label="Clear company"
|
|
disabled={isSubmitting}
|
|
>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="14"
|
|
height="14"
|
|
>
|
|
<path d="M18 6L6 18M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{:else}
|
|
<div
|
|
class="co-search-input-wrap"
|
|
bind:this={companyInputWrapEl}
|
|
>
|
|
<svg
|
|
class="co-search-input-icon"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="14"
|
|
height="14"
|
|
>
|
|
<circle cx="11" cy="11" r="8" />
|
|
<path d="M21 21l-4.35-4.35" />
|
|
</svg>
|
|
<input
|
|
id="co-company-search"
|
|
type="text"
|
|
bind:value={companySearchInput}
|
|
on:input={handleCompanySearch}
|
|
on:focus={() => {
|
|
if (companySearchInput.trim()) {
|
|
showCompanyDropdown = true;
|
|
requestAnimationFrame(updateDropdownPosition);
|
|
}
|
|
}}
|
|
placeholder="Search for a company…"
|
|
disabled={isSubmitting}
|
|
autocomplete="off"
|
|
/>
|
|
{#if isSearchingCompanies}
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="14"
|
|
height="14"
|
|
class="co-spin-icon co-search-spinner"
|
|
>
|
|
<circle
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
fill="none"
|
|
opacity="0.25"
|
|
/>
|
|
<path
|
|
d="M12 2a10 10 0 0 1 10 10"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
fill="none"
|
|
stroke-linecap="round"
|
|
/>
|
|
</svg>
|
|
{/if}
|
|
</div>
|
|
{#if showCompanyDropdown && companies.length > 0}
|
|
<ul class="co-company-dropdown" style={dropdownStyle}>
|
|
{#each companies as c}
|
|
<li>
|
|
<button
|
|
type="button"
|
|
on:click={() => selectCompany(c)}
|
|
>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="13"
|
|
height="13"
|
|
>
|
|
<path
|
|
d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"
|
|
/>
|
|
</svg>
|
|
{c.name}
|
|
</button>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{:else if showCompanyDropdown && companySearchInput.trim() && !isSearchingCompanies}
|
|
<ul class="co-company-dropdown" style={dropdownStyle}>
|
|
<li class="co-company-empty">No companies found</li>
|
|
</ul>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Contact & Site -->
|
|
{:else if currentStep === 2}
|
|
<div class="co-step-content" style="animation: coFadeIn 0.2s ease">
|
|
<div class="co-section">
|
|
<h3 class="co-section-title">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="15"
|
|
height="15"
|
|
>
|
|
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" />
|
|
<circle cx="12" cy="7" r="4" />
|
|
</svg>
|
|
Company Contact
|
|
</h3>
|
|
|
|
{#if isLoadingCompanyDetails}
|
|
<div class="co-loading-field">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="14"
|
|
height="14"
|
|
class="co-spin-icon"
|
|
>
|
|
<circle
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
fill="none"
|
|
opacity="0.25"
|
|
/>
|
|
<path
|
|
d="M12 2a10 10 0 0 1 10 10"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
fill="none"
|
|
stroke-linecap="round"
|
|
/>
|
|
</svg>
|
|
Loading company details…
|
|
</div>
|
|
{:else}
|
|
<div class="co-form-grid">
|
|
<div class="co-form-group co-full-width">
|
|
<label for="co-contact">Contact</label>
|
|
<select
|
|
id="co-contact"
|
|
bind:value={selectedContactId}
|
|
disabled={isSubmitting}
|
|
>
|
|
<option value="">— None —</option>
|
|
{#each activeContacts as c}
|
|
<option value={String(c.cwId)}>
|
|
{c.firstName}
|
|
{c.lastName}{c.title ? ` — ${c.title}` : ""}
|
|
</option>
|
|
{/each}
|
|
{#if contacts.some((c) => c.inactive)}
|
|
<optgroup label="Inactive">
|
|
{#each contacts.filter((c) => c.inactive) as c}
|
|
<option value={String(c.cwId)}>
|
|
{c.firstName}
|
|
{c.lastName}{c.title ? ` — ${c.title}` : ""} (inactive)
|
|
</option>
|
|
{/each}
|
|
</optgroup>
|
|
{/if}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{#if selectedContact}
|
|
<div class="co-contact-details">
|
|
<div class="co-contact-details-header">
|
|
<div class="co-contact-avatar">
|
|
{(selectedContact.firstName?.[0] ?? "").toUpperCase()}{(
|
|
selectedContact.lastName?.[0] ?? ""
|
|
).toUpperCase()}
|
|
</div>
|
|
<div>
|
|
<p class="co-contact-name">
|
|
{selectedContact.firstName}
|
|
{selectedContact.lastName}
|
|
</p>
|
|
{#if selectedContact.title}
|
|
<p class="co-contact-title">
|
|
{selectedContact.title}
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<div class="co-contact-info-grid">
|
|
{#if selectedContact.email}
|
|
<div class="co-contact-info-item">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="13"
|
|
height="13"
|
|
>
|
|
<path
|
|
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"
|
|
/>
|
|
<polyline points="22,6 12,13 2,6" />
|
|
</svg>
|
|
<span>{selectedContact.email}</span>
|
|
</div>
|
|
{/if}
|
|
{#if selectedContact.phone}
|
|
<div class="co-contact-info-item">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="13"
|
|
height="13"
|
|
>
|
|
<path
|
|
d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 16.92z"
|
|
/>
|
|
</svg>
|
|
<span>{selectedContact.phone}</span>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if contacts.length === 0 && !isLoadingCompanyDetails}
|
|
<div class="co-empty-state">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.5"
|
|
width="24"
|
|
height="24"
|
|
>
|
|
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" />
|
|
<circle cx="12" cy="7" r="4" />
|
|
</svg>
|
|
<p>No contacts found for this company</p>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="co-section">
|
|
<h3 class="co-section-title">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="15"
|
|
height="15"
|
|
>
|
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" />
|
|
<circle cx="12" cy="10" r="3" />
|
|
</svg>
|
|
Company Site
|
|
</h3>
|
|
|
|
{#if isLoadingCompanyDetails}
|
|
<div class="co-loading-field">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="14"
|
|
height="14"
|
|
class="co-spin-icon"
|
|
>
|
|
<circle
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
fill="none"
|
|
opacity="0.25"
|
|
/>
|
|
<path
|
|
d="M12 2a10 10 0 0 1 10 10"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
fill="none"
|
|
stroke-linecap="round"
|
|
/>
|
|
</svg>
|
|
Loading site information…
|
|
</div>
|
|
{:else if companyAddress}
|
|
<div class="co-site-card">
|
|
<div class="co-site-card-icon">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.5"
|
|
width="20"
|
|
height="20"
|
|
>
|
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" />
|
|
<circle cx="12" cy="10" r="3" />
|
|
</svg>
|
|
</div>
|
|
<div class="co-site-card-details">
|
|
<p class="co-site-label">Primary Address</p>
|
|
<p class="co-site-line">{companyAddress.line1 ?? ""}</p>
|
|
{#if companyAddress.line2}
|
|
<p class="co-site-line">{companyAddress.line2}</p>
|
|
{/if}
|
|
<p class="co-site-line">
|
|
{companyAddress.city ?? ""}{companyAddress.city &&
|
|
companyAddress.state
|
|
? ", "
|
|
: ""}{companyAddress.state ?? ""}
|
|
{companyAddress.zip ?? ""}
|
|
</p>
|
|
{#if companyAddress.country && companyAddress.country !== "United States"}
|
|
<p class="co-site-line">{companyAddress.country}</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="co-empty-state">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.5"
|
|
width="24"
|
|
height="24"
|
|
>
|
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" />
|
|
<circle cx="12" cy="10" r="3" />
|
|
</svg>
|
|
<p>No address on file for this company</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3: Review -->
|
|
{:else if currentStep === 3}
|
|
<div class="co-step-content" style="animation: coFadeIn 0.2s ease">
|
|
<div class="co-review">
|
|
<div class="co-review-header">
|
|
<div class="co-review-icon">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.5"
|
|
width="28"
|
|
height="28"
|
|
>
|
|
<path
|
|
d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<h3 class="co-review-title">{name}</h3>
|
|
</div>
|
|
|
|
<div class="co-review-grid">
|
|
<div class="co-review-card">
|
|
<h4>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="14"
|
|
height="14"
|
|
>
|
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
|
<line x1="16" y1="2" x2="16" y2="6" />
|
|
<line x1="8" y1="2" x2="8" y2="6" />
|
|
<line x1="3" y1="10" x2="21" y2="10" />
|
|
</svg>
|
|
Details
|
|
</h4>
|
|
<dl>
|
|
<div class="co-review-item">
|
|
<dt>Expected Close</dt>
|
|
<dd>
|
|
{new Date(
|
|
expectedCloseDate + "T00:00:00",
|
|
).toLocaleDateString("en-US", {
|
|
month: "long",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
})}
|
|
</dd>
|
|
</div>
|
|
{#if source}
|
|
<div class="co-review-item">
|
|
<dt>Source</dt>
|
|
<dd>{source}</dd>
|
|
</div>
|
|
{/if}
|
|
{#if customerPO}
|
|
<div class="co-review-item">
|
|
<dt>Customer PO</dt>
|
|
<dd class="mono">{customerPO}</dd>
|
|
</div>
|
|
{/if}
|
|
</dl>
|
|
</div>
|
|
|
|
<div class="co-review-card">
|
|
<h4>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="14"
|
|
height="14"
|
|
>
|
|
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" />
|
|
<circle cx="9" cy="7" r="4" />
|
|
</svg>
|
|
Assignment
|
|
</h4>
|
|
<dl>
|
|
<div class="co-review-item">
|
|
<dt>Primary Rep</dt>
|
|
<dd>{selectedPrimaryRep?.name || "—"}</dd>
|
|
</div>
|
|
{#if selectedSecondaryRep}
|
|
<div class="co-review-item">
|
|
<dt>Secondary Rep</dt>
|
|
<dd>{selectedSecondaryRep.name}</dd>
|
|
</div>
|
|
{/if}
|
|
<div class="co-review-item">
|
|
<dt>Company</dt>
|
|
<dd>{companyName || "—"}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<div class="co-review-card">
|
|
<h4>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="14"
|
|
height="14"
|
|
>
|
|
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" />
|
|
<circle cx="12" cy="7" r="4" />
|
|
</svg>
|
|
Contact & Site
|
|
</h4>
|
|
<dl>
|
|
<div class="co-review-item">
|
|
<dt>Contact</dt>
|
|
<dd>
|
|
{selectedContact
|
|
? `${selectedContact.firstName} ${selectedContact.lastName}`
|
|
: "—"}
|
|
</dd>
|
|
</div>
|
|
{#if companyAddress}
|
|
<div class="co-review-item">
|
|
<dt>Site</dt>
|
|
<dd>
|
|
{companyAddress.city ?? ""}{companyAddress.city &&
|
|
companyAddress.state
|
|
? ", "
|
|
: ""}{companyAddress.state ?? ""}
|
|
</dd>
|
|
</div>
|
|
{/if}
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
|
|
{#if description}
|
|
<div class="co-review-notes">
|
|
<h4>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="14"
|
|
height="14"
|
|
>
|
|
<path
|
|
d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"
|
|
/>
|
|
<path
|
|
d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"
|
|
/>
|
|
</svg>
|
|
Description
|
|
</h4>
|
|
<p>{description}</p>
|
|
</div>
|
|
{/if}
|
|
|
|
<label class="co-navigate-check">
|
|
<input type="checkbox" bind:checked={navigateOnCreate} />
|
|
<span>Open opportunity after creating</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- ── Footer ── -->
|
|
<div class="co-modal-footer">
|
|
{#if currentStep > 0}
|
|
<button
|
|
class="co-btn-back"
|
|
on:click={prevStep}
|
|
disabled={isSubmitting}
|
|
type="button"
|
|
>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="14"
|
|
height="14"
|
|
>
|
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
</svg>
|
|
Back
|
|
</button>
|
|
{/if}
|
|
|
|
<div class="co-footer-spacer"></div>
|
|
|
|
<button
|
|
class="co-btn-cancel"
|
|
on:click={handleClose}
|
|
disabled={isSubmitting}
|
|
type="button"
|
|
>
|
|
Cancel
|
|
</button>
|
|
|
|
{#if currentStep < 3}
|
|
<button
|
|
class="co-btn-next"
|
|
on:click={nextStep}
|
|
disabled={(currentStep === 0 && !isStep0Valid) ||
|
|
(currentStep === 1 && !isStep1Valid) ||
|
|
(currentStep === 2 && !isStep2Valid)}
|
|
type="button"
|
|
>
|
|
{currentStep === 0
|
|
? "Next: Assignment"
|
|
: currentStep === 1
|
|
? "Next: Contact & Site"
|
|
: "Review"}
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="14"
|
|
height="14"
|
|
>
|
|
<path d="M5 12h14M12 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
{:else}
|
|
<button
|
|
class="co-btn-submit"
|
|
on:click={handleSubmit}
|
|
disabled={!isValid || isSubmitting}
|
|
type="button"
|
|
>
|
|
{#if isSubmitting}
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="14"
|
|
height="14"
|
|
class="co-spin-icon"
|
|
>
|
|
<circle
|
|
cx="12"
|
|
cy="12"
|
|
r="10"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
fill="none"
|
|
opacity="0.25"
|
|
/>
|
|
<path
|
|
d="M12 2a10 10 0 0 1 10 10"
|
|
stroke="currentColor"
|
|
stroke-width="3"
|
|
fill="none"
|
|
stroke-linecap="round"
|
|
/>
|
|
</svg>
|
|
Creating…
|
|
{:else}
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
width="14"
|
|
height="14"
|
|
>
|
|
<path
|
|
d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z"
|
|
/>
|
|
</svg>
|
|
Create Opportunity
|
|
{/if}
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
/* ── Backdrop ── */
|
|
.co-modal-backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 9999;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(0, 0, 0, 0.55);
|
|
backdrop-filter: blur(6px);
|
|
-webkit-backdrop-filter: blur(6px);
|
|
animation: coBackdropIn 0.2s ease;
|
|
}
|
|
|
|
/* ── Modal shell ── */
|
|
.co-modal {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 640px;
|
|
max-width: 94vw;
|
|
max-height: 88vh;
|
|
background: var(--bg-surface, #ffffff);
|
|
border: 1px solid var(--border-subtle, #e5e5e5);
|
|
border-radius: 16px;
|
|
box-shadow:
|
|
0 24px 80px -12px rgba(0, 0, 0, 0.28),
|
|
0 0 0 1px rgba(0, 0, 0, 0.03);
|
|
animation: coModalIn 0.25s cubic-bezier(0.32, 0.72, 0, 1);
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ── Header ── */
|
|
.co-modal-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 20px 24px 16px;
|
|
border-bottom: 1px solid var(--border-subtle, #e5e5e5);
|
|
background: linear-gradient(
|
|
135deg,
|
|
rgba(59, 130, 246, 0.04) 0%,
|
|
rgba(168, 85, 247, 0.04) 100%
|
|
);
|
|
}
|
|
|
|
.co-modal-title-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
}
|
|
|
|
.co-modal-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 42px;
|
|
height: 42px;
|
|
border-radius: 12px;
|
|
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
|
color: #ffffff;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.co-modal-title-group h2 {
|
|
margin: 0;
|
|
font-size: 17px;
|
|
font-weight: 650;
|
|
color: var(--text-primary, #1a1a1a);
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.co-modal-subtitle {
|
|
margin: 2px 0 0;
|
|
font-size: 12.5px;
|
|
color: var(--text-muted, #888);
|
|
font-weight: 400;
|
|
}
|
|
|
|
.co-close-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 32px;
|
|
height: 32px;
|
|
padding: 0;
|
|
background: none;
|
|
border: 1px solid transparent;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
color: var(--text-muted, #888);
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.co-close-btn:hover {
|
|
background: var(--card-hover-bg, #f5f5f5);
|
|
color: var(--text-primary, #1a1a1a);
|
|
border-color: var(--border-subtle, #e5e5e5);
|
|
}
|
|
|
|
/* ── Step indicator ── */
|
|
.co-steps {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0;
|
|
padding: 16px 24px;
|
|
border-bottom: 1px solid var(--border-subtle, #e5e5e5);
|
|
background: var(--bg-inset, #fafafa);
|
|
}
|
|
|
|
.co-step {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 14px;
|
|
border: none;
|
|
background: none;
|
|
cursor: pointer;
|
|
border-radius: 8px;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.co-step:hover:not(.disabled) {
|
|
background: var(--card-hover-bg, #f0f0f0);
|
|
}
|
|
|
|
.co-step.disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.co-step-number {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
border: 2px solid var(--border-subtle, #ddd);
|
|
color: var(--text-muted, #888);
|
|
background: var(--bg-surface, #fff);
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.co-step.active .co-step-number {
|
|
border-color: #3b82f6;
|
|
background: #3b82f6;
|
|
color: #fff;
|
|
}
|
|
|
|
.co-step.completed .co-step-number,
|
|
.co-step-number.check {
|
|
border-color: #22c55e;
|
|
background: #22c55e;
|
|
color: #fff;
|
|
}
|
|
|
|
.co-step-label {
|
|
font-size: 12.5px;
|
|
font-weight: 550;
|
|
color: var(--text-secondary, #666);
|
|
transition: color 0.15s;
|
|
}
|
|
|
|
.co-step.active .co-step-label {
|
|
color: var(--text-primary, #1a1a1a);
|
|
}
|
|
|
|
.co-step-connector {
|
|
width: 32px;
|
|
height: 2px;
|
|
background: var(--border-subtle, #ddd);
|
|
border-radius: 1px;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.co-step-connector.filled {
|
|
background: #22c55e;
|
|
}
|
|
|
|
/* ── Body ── */
|
|
.co-modal-body {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 20px 24px;
|
|
}
|
|
|
|
.co-step-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
/* ── Sections ── */
|
|
.co-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.co-section-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin: 0;
|
|
font-size: 13px;
|
|
font-weight: 650;
|
|
color: var(--text-primary, #1a1a1a);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
.co-section-title svg {
|
|
color: var(--text-muted, #888);
|
|
}
|
|
|
|
/* ── Form grid ── */
|
|
.co-form-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 14px;
|
|
}
|
|
|
|
.co-full-width {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.co-form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.co-form-group label {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary, #666);
|
|
letter-spacing: 0.01em;
|
|
}
|
|
|
|
.co-req {
|
|
color: #ef4444;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.co-form-group input[type="text"],
|
|
.co-form-group input[type="date"],
|
|
.co-form-group select {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--border-subtle, #ddd);
|
|
border-radius: 8px;
|
|
background: var(--bg-inset, #fafafa);
|
|
font-size: 13px;
|
|
color: var(--text-primary, #1a1a1a);
|
|
box-sizing: border-box;
|
|
transition:
|
|
border-color 0.15s,
|
|
box-shadow 0.15s,
|
|
background 0.15s;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.co-form-group input:focus,
|
|
.co-form-group select:focus {
|
|
outline: none;
|
|
border-color: #3b82f6;
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
|
|
background: var(--bg-surface, #fff);
|
|
}
|
|
|
|
.co-form-group input::placeholder {
|
|
color: var(--text-muted, #aaa);
|
|
}
|
|
|
|
.co-form-group input:disabled,
|
|
.co-form-group select:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.co-form-group select {
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2.5'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
|
background-repeat: no-repeat;
|
|
background-position: right 10px center;
|
|
padding-right: 30px;
|
|
}
|
|
|
|
/* ── Notes textarea ── */
|
|
.co-notes-textarea {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border: 1px solid var(--border-subtle, #ddd);
|
|
border-radius: 8px;
|
|
background: var(--bg-inset, #fafafa);
|
|
font-size: 13px;
|
|
color: var(--text-primary, #1a1a1a);
|
|
box-sizing: border-box;
|
|
resize: vertical;
|
|
font-family: inherit;
|
|
line-height: 1.5;
|
|
transition:
|
|
border-color 0.15s,
|
|
box-shadow 0.15s;
|
|
}
|
|
|
|
.co-notes-textarea:focus {
|
|
outline: none;
|
|
border-color: #3b82f6;
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
|
|
background: var(--bg-surface, #fff);
|
|
}
|
|
|
|
.co-notes-textarea::placeholder {
|
|
color: var(--text-muted, #aaa);
|
|
}
|
|
|
|
.co-notes-textarea:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* ── Loading field ── */
|
|
.co-loading-field {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--border-subtle, #ddd);
|
|
border-radius: 8px;
|
|
background: var(--bg-inset, #fafafa);
|
|
font-size: 12.5px;
|
|
color: var(--text-muted, #888);
|
|
}
|
|
|
|
.co-spin-icon {
|
|
animation: coSpin 0.8s linear infinite;
|
|
color: var(--text-muted, #888);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Company search ── */
|
|
.co-company-search-wrap {
|
|
position: relative;
|
|
}
|
|
|
|
.co-search-input-wrap {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.co-search-input-icon {
|
|
position: absolute;
|
|
left: 10px;
|
|
color: var(--text-muted, #aaa);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.co-search-input-wrap input {
|
|
padding-left: 32px !important;
|
|
}
|
|
|
|
.co-search-spinner {
|
|
position: absolute;
|
|
right: 10px;
|
|
}
|
|
|
|
.co-company-dropdown {
|
|
position: fixed;
|
|
list-style: none;
|
|
padding: 4px;
|
|
background: var(--bg-surface, #fff);
|
|
border: 1px solid var(--border-subtle, #ddd);
|
|
border-radius: 10px;
|
|
box-shadow:
|
|
0 8px 24px -4px rgba(0, 0, 0, 0.12),
|
|
0 0 0 1px rgba(0, 0, 0, 0.03);
|
|
z-index: 10001;
|
|
max-height: 180px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.co-company-dropdown li button {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
padding: 8px 10px;
|
|
background: none;
|
|
border: none;
|
|
border-radius: 7px;
|
|
font-size: 13px;
|
|
color: var(--text-primary, #1a1a1a);
|
|
cursor: pointer;
|
|
text-align: left;
|
|
transition: background 0.1s;
|
|
}
|
|
|
|
.co-company-dropdown li button:hover {
|
|
background: var(--card-hover-bg, #f5f5f5);
|
|
}
|
|
|
|
.co-company-dropdown li button svg {
|
|
color: var(--text-muted, #aaa);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.co-company-empty {
|
|
padding: 12px 10px;
|
|
font-size: 12.5px;
|
|
color: var(--text-muted, #888);
|
|
text-align: center;
|
|
}
|
|
|
|
.co-selected-company {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.co-company-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 12px;
|
|
border-radius: 8px;
|
|
background: rgba(59, 130, 246, 0.08);
|
|
border: 1px solid rgba(59, 130, 246, 0.2);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-primary, #1a1a1a);
|
|
}
|
|
|
|
.co-company-chip svg {
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.co-company-clear {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 28px;
|
|
height: 28px;
|
|
padding: 0;
|
|
background: none;
|
|
border: 1px solid transparent;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
color: var(--text-muted, #888);
|
|
transition: all 0.1s;
|
|
}
|
|
|
|
.co-company-clear:hover:not(:disabled) {
|
|
background: rgba(220, 38, 38, 0.06);
|
|
color: #dc2626;
|
|
}
|
|
|
|
.co-company-clear:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* ── Contact details ── */
|
|
.co-contact-details {
|
|
border: 1px solid var(--border-subtle, #e5e5e5);
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
background: var(--bg-inset, #fafafa);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
}
|
|
|
|
.co-contact-details-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.co-contact-avatar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
|
color: #fff;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.02em;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.co-contact-name {
|
|
margin: 0;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--text-primary, #1a1a1a);
|
|
}
|
|
|
|
.co-contact-title {
|
|
margin: 2px 0 0;
|
|
font-size: 12px;
|
|
color: var(--text-muted, #888);
|
|
}
|
|
|
|
.co-contact-info-grid {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
padding-left: 4px;
|
|
}
|
|
|
|
.co-contact-info-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 12.5px;
|
|
color: var(--text-secondary, #666);
|
|
}
|
|
|
|
.co-contact-info-item svg {
|
|
color: var(--text-muted, #aaa);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Site card ── */
|
|
.co-site-card {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 14px;
|
|
border: 1px solid var(--border-subtle, #e5e5e5);
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
background: var(--bg-inset, #fafafa);
|
|
}
|
|
|
|
.co-site-card-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 10px;
|
|
background: rgba(34, 197, 94, 0.08);
|
|
color: #22c55e;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.co-site-card-details {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.co-site-label {
|
|
margin: 0 0 4px;
|
|
font-size: 11px;
|
|
font-weight: 650;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: var(--text-muted, #888);
|
|
}
|
|
|
|
.co-site-line {
|
|
margin: 0;
|
|
font-size: 13px;
|
|
color: var(--text-primary, #1a1a1a);
|
|
line-height: 1.45;
|
|
}
|
|
|
|
/* ── Empty state ── */
|
|
.co-empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 24px 16px;
|
|
border: 1px dashed var(--border-subtle, #ddd);
|
|
border-radius: 12px;
|
|
background: var(--bg-inset, #fafafa);
|
|
}
|
|
|
|
.co-empty-state svg {
|
|
color: var(--text-muted, #bbb);
|
|
}
|
|
|
|
.co-empty-state p {
|
|
margin: 0;
|
|
font-size: 13px;
|
|
color: var(--text-muted, #888);
|
|
}
|
|
|
|
/* ── Review ── */
|
|
.co-review {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
}
|
|
|
|
.co-review-header {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 10px;
|
|
text-align: center;
|
|
padding: 8px 0 4px;
|
|
}
|
|
|
|
.co-review-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 16px;
|
|
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
|
color: #fff;
|
|
box-shadow: 0 4px 16px -2px rgba(59, 130, 246, 0.3);
|
|
}
|
|
|
|
.co-review-title {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 650;
|
|
color: var(--text-primary, #1a1a1a);
|
|
letter-spacing: -0.01em;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.co-review-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.co-review-card {
|
|
border: 1px solid var(--border-subtle, #e5e5e5);
|
|
border-radius: 12px;
|
|
padding: 14px 16px;
|
|
background: var(--bg-inset, #fafafa);
|
|
}
|
|
|
|
.co-review-card h4,
|
|
.co-review-notes h4 {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
margin: 0 0 10px;
|
|
font-size: 11.5px;
|
|
font-weight: 650;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: var(--text-muted, #888);
|
|
}
|
|
|
|
.co-review-card dl {
|
|
margin: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.co-review-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
gap: 12px;
|
|
}
|
|
|
|
.co-review-item dt {
|
|
font-size: 12px;
|
|
color: var(--text-muted, #888);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.co-review-item dd {
|
|
margin: 0;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-primary, #1a1a1a);
|
|
text-align: right;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.co-review-item dd.mono {
|
|
font-family: "SF Mono", "Fira Code", monospace;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.co-review-notes {
|
|
border: 1px solid var(--border-subtle, #e5e5e5);
|
|
border-radius: 12px;
|
|
padding: 14px 16px;
|
|
background: var(--bg-inset, #fafafa);
|
|
}
|
|
|
|
.co-review-notes p {
|
|
margin: 0;
|
|
font-size: 13px;
|
|
color: var(--text-primary, #1a1a1a);
|
|
line-height: 1.55;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.co-navigate-check {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 14px;
|
|
border-radius: 8px;
|
|
background: var(--bg-inset, #fafafa);
|
|
border: 1px solid var(--border-subtle, #e5e5e5);
|
|
cursor: pointer;
|
|
transition: background 0.1s;
|
|
}
|
|
|
|
.co-navigate-check:hover {
|
|
background: var(--card-hover-bg, #f0f0f0);
|
|
}
|
|
|
|
.co-navigate-check input {
|
|
accent-color: #3b82f6;
|
|
}
|
|
|
|
.co-navigate-check span {
|
|
font-size: 12.5px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary, #666);
|
|
}
|
|
|
|
/* ── Error banner ── */
|
|
.co-error-banner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 14px;
|
|
border-radius: 10px;
|
|
background: rgba(220, 38, 38, 0.06);
|
|
border: 1px solid rgba(220, 38, 38, 0.15);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.co-error-banner svg {
|
|
color: #dc2626;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.co-error-banner span {
|
|
flex: 1;
|
|
font-size: 13px;
|
|
color: #dc2626;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.co-error-dismiss {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 24px;
|
|
height: 24px;
|
|
padding: 0;
|
|
background: none;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
color: #dc2626;
|
|
opacity: 0.6;
|
|
transition: opacity 0.1s;
|
|
}
|
|
|
|
.co-error-dismiss:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* ── Footer ── */
|
|
.co-modal-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 14px 24px;
|
|
border-top: 1px solid var(--border-subtle, #e5e5e5);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.co-footer-spacer {
|
|
flex: 1;
|
|
}
|
|
|
|
.co-btn-back {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 7px 14px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
background: none;
|
|
border: 1px solid var(--border-subtle, #ddd);
|
|
color: var(--text-secondary, #666);
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.co-btn-back:hover:not(:disabled) {
|
|
background: var(--card-hover-bg, #f5f5f5);
|
|
border-color: var(--border-default, #ccc);
|
|
color: var(--text-primary, #1a1a1a);
|
|
}
|
|
|
|
.co-btn-cancel {
|
|
padding: 7px 16px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
background: var(--bg-inset, #fafafa);
|
|
border: 1px solid var(--border-subtle, #ddd);
|
|
color: var(--text-secondary, #666);
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.co-btn-cancel:hover:not(:disabled) {
|
|
background: var(--card-hover-bg, #f0f0f0);
|
|
border-color: var(--border-default, #ccc);
|
|
color: var(--text-primary, #1a1a1a);
|
|
}
|
|
|
|
.co-btn-cancel:disabled,
|
|
.co-btn-back:disabled {
|
|
opacity: 0.45;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.co-btn-next {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 7px 18px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 550;
|
|
cursor: pointer;
|
|
background: var(--bg-surface, #fff);
|
|
border: 1px solid #3b82f6;
|
|
color: #3b82f6;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.co-btn-next:hover:not(:disabled) {
|
|
background: rgba(59, 130, 246, 0.06);
|
|
}
|
|
|
|
.co-btn-next:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.co-btn-submit {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 8px 20px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%);
|
|
border: 1px solid transparent;
|
|
color: #fff;
|
|
transition: all 0.15s;
|
|
box-shadow: 0 2px 8px -2px rgba(59, 130, 246, 0.35);
|
|
}
|
|
|
|
.co-btn-submit:hover:not(:disabled) {
|
|
filter: brightness(1.08);
|
|
box-shadow: 0 4px 12px -2px rgba(59, 130, 246, 0.4);
|
|
transform: translateY(-0.5px);
|
|
}
|
|
|
|
.co-btn-submit:active:not(:disabled) {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.co-btn-submit:disabled {
|
|
opacity: 0.45;
|
|
cursor: not-allowed;
|
|
box-shadow: none;
|
|
}
|
|
|
|
/* ── Animations ── */
|
|
@keyframes coBackdropIn {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes coModalIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.96) translateY(8px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes coFadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(6px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
@keyframes coSpin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/* ── Mobile ── */
|
|
@media (max-width: 768px) {
|
|
.co-modal-backdrop {
|
|
align-items: flex-end;
|
|
}
|
|
|
|
.co-modal {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
max-height: 94vh;
|
|
border-radius: 16px 16px 0 0;
|
|
animation: coSheetIn 0.3s cubic-bezier(0.32, 0.72, 0, 1);
|
|
}
|
|
|
|
@keyframes coSheetIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(100%);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.co-modal-header {
|
|
padding: 16px 18px 14px;
|
|
}
|
|
|
|
.co-modal-body {
|
|
padding: 16px 18px;
|
|
}
|
|
|
|
.co-modal-footer {
|
|
padding: 12px 18px;
|
|
}
|
|
|
|
.co-steps {
|
|
padding: 12px 16px;
|
|
gap: 0;
|
|
}
|
|
|
|
.co-step {
|
|
padding: 5px 10px;
|
|
}
|
|
|
|
.co-step-label {
|
|
font-size: 11.5px;
|
|
}
|
|
|
|
.co-step-connector {
|
|
width: 20px;
|
|
}
|
|
|
|
.co-form-grid {
|
|
grid-template-columns: 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.co-review-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.co-form-group input[type="text"],
|
|
.co-form-group input[type="date"],
|
|
.co-form-group select {
|
|
font-size: 16px;
|
|
padding: 10px 12px;
|
|
}
|
|
|
|
.co-notes-textarea {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.co-btn-cancel,
|
|
.co-btn-next,
|
|
.co-btn-submit,
|
|
.co-btn-back {
|
|
padding: 10px 18px;
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
</style>
|