Sales Contact Form
blockmarketingInteractive enterprise solutions architect dispatch workbench with track selection, live hub timezones, SLA badges, and ticket generator.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/contact-01.json$npx shadcn@latest add https://uipkge.dev/r/lit/contact-01.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/contact-01.json$bunx shadcn@latest add https://uipkge.dev/r/lit/contact-01.jsonManifest:
https://uipkge.dev/r/lit/contact-01.jsonInstalls to:src/uip/blocks/contact-01/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ContactSubmitPayloadinterface ContactSubmitPayload {
type: string
name: string
email: string
company: string
teamSize: string
message: string
ticketId: string
}InquiryTypeinterface InquiryType {
id: string
title: string
subtitle: string
sla: string
icon: IconNode
iconName: string
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-contact-01': UipContact01
}
}Files installed (1)
src/uip/blocks/contact-01/contact-01.ts18.7 kB
import { LitElement, css, html } from 'lit' import { Building2, Check, Copy, Headphones, MessageSquare, Send, ShieldCheck, type IconNode, } from 'lucide' import { cn } from '../../lib/utils' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' // Side-effect imports: register the composed elements for standalone use // (consumers who install only this block still get working primitives). import '../../components/badge/badge' import '../../components/button/button' import '../../components/input/input' import type { UipInput } from '../../components/input/input' import '../../components/label/label' import '../../components/textarea/textarea' import type { UipTextarea } from '../../components/textarea/textarea' export interface ContactSubmitPayload { type: string name: string email: string company: string teamSize: string message: string ticketId: string } interface InquiryType { id: string title: string subtitle: string sla: string icon: IconNode iconName: string } const inquiryTypes: InquiryType[] = [ { id: 'enterprise', title: 'Enterprise Architecture', subtitle: 'Custom registries, zero-lockin migrations & multi-team governance', sla: '< 2h SLA', icon: Building2, iconName: 'building-2', }, { id: 'support', title: 'Dedicated Engineering SLA', subtitle: '24/7 incident response, custom upstream bugfixes & private Slack channel', sla: '< 15m SLA', icon: Headphones, iconName: 'headphones', }, { id: 'security', title: 'Security & Compliance Review', subtitle: 'SOC2 Type II audits, self-hosted registry mirrors & air-gapped deployments', sla: '< 4h SLA', icon: ShieldCheck, iconName: 'shield-check', }, ] const globalHubs = [ { city: 'San Francisco', tz: 'PST (UTC-8)', status: 'Active (09:00 - 18:00)', email: 'sf@uipkge.dev' }, { city: 'London', tz: 'GMT (UTC+0)', status: 'Active (08:30 - 17:30)', email: 'london@uipkge.dev' }, { city: 'Singapore', tz: 'SGT (UTC+8)', status: 'Active (09:00 - 18:00)', email: 'singapore@uipkge.dev' }, ] /** * <uip-contact-01> — the registry Contact01 block as a web component. * * Class strings are React's Contact01 verbatim. React state (useState) * becomes @state fields. `onSubmit` (payload) becomes a `submit` * CustomEvent with the payload in `detail` (bubbles, composed); the ticket * copy timeout is cleared in `disconnectedCallback`. Card chrome stays plain * divs carrying the registry Card part classes (see the login-01 block). */ export class UipContact01 extends LitElement { static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } // :host display can't be a utility class (the host has no template of its own). static styles = [tailwind, css`:host { display: block; }`] static properties = { selectedType: { state: true }, name: { state: true }, email: { state: true }, company: { state: true }, teamSize: { state: true }, message: { state: true }, sent: { state: true }, ticketId: { state: true }, copiedTicket: { state: true }, } private selectedType = 'enterprise' private name = '' private email = '' private company = '' private teamSize = '10-50' private message = '' private sent = false private ticketId = '' private copiedTicket = false private timer?: number constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'contact-01') } disconnectedCallback() { super.disconnectedCallback() if (this.timer) window.clearTimeout(this.timer) } private get canSubmit() { return !!this.name.trim() && !!this.email.trim() && !!this.message.trim() } private handleSubmit(e: Event) { e.preventDefault() if (!this.canSubmit) return const generatedId = `UIP-${Math.floor(1000 + Math.random() * 9000)}` this.ticketId = generatedId this.dispatchEvent( new CustomEvent<ContactSubmitPayload>('submit', { detail: { type: this.selectedType, name: this.name, email: this.email, company: this.company, teamSize: this.teamSize, message: this.message, ticketId: generatedId, }, bubbles: true, composed: true, }), ) this.sent = true } private copyTicket() { try { navigator.clipboard?.writeText(this.ticketId)?.catch(() => {}) } catch { // Clipboard unavailable (insecure context): still flash the label. } this.copiedTicket = true if (this.timer) window.clearTimeout(this.timer) this.timer = window.setTimeout(() => { this.copiedTicket = false }, 2000) } render() { return html` <section data-slot="contact-01" class="bg-background relative overflow-hidden px-4 py-16 sm:px-6 lg:px-8"> <div class="mx-auto max-w-6xl space-y-12"> <div class="max-w-3xl space-y-3"> <uip-badge variant="secondary" class="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs"> ${icon(ShieldCheck, 'shield-check', 'text-primary size-3.5')} Enterprise Engineering & Architecture Solutions </uip-badge> <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl"> Talk to a principal design engineer. </h2> <p class="text-muted-foreground text-sm leading-relaxed sm:text-base"> No generic sales reps. Connect directly with the core team that architects our dual-framework registry, token engines, and headless workbenches. </p> </div> <div class="grid grid-cols-1 items-start gap-8 lg:grid-cols-12"> <div class="space-y-6 lg:col-span-5"> <div class="space-y-3"> <p class="text-muted-foreground font-mono text-xs tracking-wider uppercase"> Select Engagement Track </p> <div class="space-y-2.5"> ${inquiryTypes.map((track) => { const isSelected = this.selectedType === track.id return html`<div class=${cn( 'group cursor-pointer rounded-xl border p-4 transition-colors', isSelected ? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1' : 'border-border bg-card hover:bg-muted/40', )} @click=${() => { this.selectedType = track.id }} > <div class="flex items-start justify-between gap-3"> <div class="flex items-center gap-2.5"> <div class=${cn( 'rounded-lg p-2 transition-colors', isSelected ? 'bg-primary text-primary-foreground' : 'bg-muted text-foreground group-hover:bg-primary/10 group-hover:text-primary', )} > ${icon(track.icon, track.iconName, 'size-4')} </div> <span class="text-foreground text-xs font-semibold">${track.title}</span> </div> <uip-badge variant="outline" class="bg-background font-mono text-xs">${track.sla}</uip-badge> </div> <p class="text-muted-foreground mt-2 pl-9 text-xs">${track.subtitle}</p> </div>` })} </div> </div> <div data-uipkge="" data-slot="separator" data-orientation="horizontal" role="none" class="bg-border shrink-0 h-px w-full" ></div> <div class="space-y-3"> <div class="flex items-center justify-between"> <p class="text-muted-foreground font-mono text-xs tracking-wider uppercase"> Direct Engineering Hubs </p> <div class="text-success flex items-center gap-1.5 font-mono text-xs"> <span class="bg-success size-2 animate-pulse rounded-full"></span> Live Dispatch Active </div> </div> <div class="space-y-2"> ${globalHubs.map( (hub) => html`<div class="border-border bg-card/60 flex items-center justify-between rounded-lg border p-3 text-xs shadow-xs" > <div> <p class="text-foreground font-medium">${hub.city}</p> <p class="text-muted-foreground font-mono text-xs">${hub.tz} • ${hub.status}</p> </div> <a href=${`mailto:${hub.email}`} class="text-primary font-mono text-xs hover:underline"> ${hub.email} </a> </div>`, )} </div> </div> </div> <div class="lg:col-span-7"> <div data-uipkge="" data-slot="card" class="bg-card text-card-foreground rounded-xl border border-border overflow-hidden shadow-sm transition-colors duration-150" > <div data-uipkge="" data-slot="card-header" class="grid auto-rows-min grid-cols-[minmax(0,1fr)] grid-rows-[auto_auto] items-start gap-1.5 p-6 pb-4 border-b border-border/60 bg-muted/20 has-data-[slot=card-action]:grid-cols-[minmax(0,1fr)_auto]" > <div class="flex items-center justify-between"> <h3 data-uipkge="" data-slot="card-title" class="leading-none tracking-tight flex items-center gap-2 text-base font-semibold" > ${icon(MessageSquare, 'message-square', 'text-primary size-4')} Dispatch Engineering Request </h3> <uip-badge variant="secondary" class="font-mono text-xs">Direct Dispatch: ${this.selectedType}</uip-badge> </div> <p data-uipkge="" data-slot="card-description" class="text-muted-foreground text-xs"> Provide architectural specifications or questions. Our engineering team responds within guaranteed SLA. </p> </div> <div data-uipkge="" data-slot="card-content" class="p-6 pt-0"> ${!this.sent ? html`<form @submit=${this.handleSubmit} class="space-y-4"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div class="space-y-1.5"> <uip-label for="contact-name" class="text-xs font-medium">Your Name</uip-label> <uip-input id="contact-name" .value=${this.name} @input=${(e: Event) => { this.name = (e.target as UipInput).value }} placeholder="Jane Doe" required class="h-9 text-xs" ></uip-input> </div> <div class="space-y-1.5"> <uip-label for="contact-email" class="text-xs font-medium">Work Email</uip-label> <uip-input id="contact-email" type="email" .value=${this.email} @input=${(e: Event) => { this.email = (e.target as UipInput).value }} placeholder="jane@company.com" required class="h-9 font-mono text-xs" ></uip-input> </div> </div> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div class="space-y-1.5"> <uip-label for="contact-company" class="text-xs font-medium" >Company / Organization</uip-label > <uip-input id="contact-company" .value=${this.company} @input=${(e: Event) => { this.company = (e.target as UipInput).value }} placeholder="Acme Corp" class="h-9 text-xs" ></uip-input> </div> <div class="space-y-1.5"> <uip-label class="text-xs font-medium">Engineering Team Size</uip-label> <div class="grid grid-cols-3 gap-1.5"> ${['1-10', '10-50', '50+'].map( (ts) => html`<button type="button" class=${cn( 'rounded-md border px-2 py-1.5 text-center font-mono text-xs transition-colors', this.teamSize === ts ? 'border-primary bg-primary/10 text-primary font-semibold' : 'border-border bg-background text-muted-foreground hover:text-foreground', )} @click=${() => { this.teamSize = ts }} > ${ts} </button>`, )} </div> </div> </div> <div class="space-y-1.5"> <uip-label for="contact-message" class="text-xs font-medium" >Project Scope & Technical Details</uip-label > <uip-textarea id="contact-message" .value=${this.message} @input=${(e: Event) => { this.message = (e.target as UipTextarea).value ?? '' }} rows="4" placeholder="Describe your tech stack (Vue 3, Nuxt 3, React 19, Next.js), design token requirements, and deployment targets..." required class="resize-none text-xs" ></uip-textarea> </div> <div class="flex flex-col items-center justify-between gap-3 pt-2 sm:flex-row"> <div class="text-muted-foreground flex items-center gap-2 text-xs"> ${icon(ShieldCheck, 'shield-check', 'text-success size-4')} <span>NDA & IP protection pre-guaranteed.</span> </div> <uip-button type="submit" ?disabled=${!this.canSubmit} class="h-9 w-full gap-1.5 px-5 text-xs font-semibold sm:w-auto" > <span>Dispatch Inquiry</span> ${icon(Send, 'send', 'size-3.5')} </uip-button> </div> </form>` : html`<div class="space-y-5 px-4 py-8 text-center"> <div class="bg-success/10 text-success mx-auto flex size-12 items-center justify-center rounded-full"> ${icon(Check, 'check', 'size-6')} </div> <div class="space-y-1"> <p class="text-foreground text-base font-bold">Inquiry Dispatched Successfully</p> <p class="text-muted-foreground mx-auto max-w-sm text-xs"> A principal solutions architect has been assigned. You will receive a technical response at <strong class="text-foreground font-mono">${this.email}</strong> within guaranteed SLA. </p> </div> <div class="border-border bg-muted/40 mx-auto flex max-w-xs items-center justify-between rounded-lg border p-3" > <div class="text-left"> <p class="text-muted-foreground font-mono text-xs uppercase">Reference Ticket ID</p> <p class="text-foreground font-mono text-xs font-bold">${this.ticketId}</p> </div> <uip-button variant="outline" size="sm" class="h-7 gap-1 font-mono text-xs" @click=${this.copyTicket}> ${this.copiedTicket ? icon(Check, 'check', 'text-success size-3') : icon(Copy, 'copy', 'size-3')} <span>${this.copiedTicket ? 'Copied' : 'Copy'}</span> </uip-button> </div> <uip-button variant="ghost" size="sm" class="text-muted-foreground text-xs" @click=${() => { this.sent = false }} > Send Another Inquiry </uip-button> </div>`} </div> </div> </div> </div> </div> </section> ` } } customElements.get('uip-contact-01') || customElements.define('uip-contact-01', UipContact01) declare global { interface HTMLElementTagNameMap { 'uip-contact-01': UipContact01 } }
Raw manifest:https://uipkge.dev/r/lit/contact-01.json