Menu

Change language

GitHub repo

Sales Contact Form

blockmarketing

Interactive enterprise solutions architect dispatch workbench with track selection, live hub timezones, SLA badges, and ticket generator.

Installation

$npx shadcn@latest add https://uipkge.dev/r/lit/contact-01.json
Manifest:https://uipkge.dev/r/lit/contact-01.jsonInstalls to:src/uip/blocks/contact-01/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

ContactSubmitPayload
interface ContactSubmitPayload {
  type: string
  name: string
  email: string
  company: string
  teamSize: string
  message: string
  ticketId: string
}
InquiryType
interface InquiryType {
  id: string
  title: string
  subtitle: string
  sla: string
  icon: IconNode
  iconName: string
}
HTMLElementTagNameMap
interface 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: '[email protected]' },
      { city: 'London', tz: 'GMT (UTC+0)', status: 'Active (08:30 - 17:30)', email: '[email protected]' },
      { city: 'Singapore', tz: 'SGT (UTC+8)', status: 'Active (09:00 - 18:00)', email: '[email protected]' },
    ]
    
    /**
     * <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} &bull; ${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="[email protected]"
                                  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