Menu

Change language

GitHub repo

Footer

blockmarketing

Site footer collection: multi-column newsletter, oversized brand statement, dense developer grid, mega newsletter, single-row minimal, and dense sitemap variants — each spelled out inline so copy lives in one place.

Installation

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

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
variantFooterVariantfooter-01optional

Schema

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

HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-footer': UipFooter
  }
}

Files installed (1)

  • src/uip/blocks/footer/footer.ts27 kB
    import { LitElement, css, html, nothing } from 'lit'
    import { ArrowRight, Boxes, Check, Globe, Layers } 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/select/select'
    import type { UipSelect } from '../../components/select/select'
    
    // lucide no longer ships brand glyphs, so they are inlined here the same way
    // the React footer blocks do it.
    const githubSvg = (cls: string) => html`<svg
      class=${cls}
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
    >
      <path
        d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.3.8-.6v-2c-3.2.7-3.9-1.5-3.9-1.5-.5-1.4-1.3-1.7-1.3-1.7-1.1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.8 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.3 1.2a11.4 11.4 0 016 0C17 4.7 18 5 18 5c.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.5-2.7 5.5-5.3 5.8.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6 4.6-1.5 7.9-5.8 7.9-10.9C23.5 5.7 18.3.5 12 .5z"
      />
    </svg>`
    
    const twitterSvg = (cls: string) => html`<svg
      class=${cls}
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
    >
      <path
        d="M18.24 2.25h3.31l-7.23 8.26 8.5 11.24h-6.66l-5.21-6.82-5.97 6.82H1.68l7.73-8.84L1.25 2.25h6.83l4.71 6.23zm-1.16 17.52h1.83L7.08 4.13H5.12z"
      />
    </svg>`
    
    const linkedinSvg = (cls: string) => html`<svg
      class=${cls}
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
    >
      <path
        d="M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28zM5.34 7.43a2.06 2.06 0 110-4.13 2.06 2.06 0 010 4.13zm1.78 13.02H3.56V9h3.56v11.45zM22.22 0H1.77C.79 0 0 .77 0 1.73v20.54C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.73V1.73C24 .77 23.2 0 22.22 0z"
      />
    </svg>`
    
    const youtubeSvg = (cls: string) => html`<svg
      class=${cls}
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
    >
      <path
        d="M23.5 6.2a3 3 0 00-2.1-2.1C19.5 3.6 12 3.6 12 3.6s-7.5 0-9.4.5A3 3 0 00.5 6.2C0 8.1 0 12 0 12s0 3.9.5 5.8a3 3 0 002.1 2.1c1.9.5 9.4.5 9.4.5s7.5 0 9.4-.5a3 3 0 002.1-2.1c.5-1.9.5-5.8.5-5.8s0-3.9-.5-5.8zM9.6 15.6V8.4l6.2 3.6-6.2 3.6z"
      />
    </svg>`
    
    const brandSections = [
      { heading: 'Platform', links: ['Metric layer', 'Governance', 'Alerting', 'Embedding'] },
      { heading: 'Company', links: ['About', 'Careers', 'Customers', 'Press kit'] },
      { heading: 'Resources', links: ['Documentation', 'Guides', 'Benchmarks', 'Status'] },
    ]
    
    const megaSections = [
      { heading: 'Product', links: ['Metric layer', 'Dashboards', 'Alerting', 'Embedding', 'Changelog'] },
      { heading: 'Solutions', links: ['Finance', 'RevOps', 'Analytics', 'Engineering'] },
      { heading: 'Developers', links: ['Documentation', 'API reference', 'CLI', 'Status'] },
      { heading: 'Company', links: ['About', 'Careers', 'Customers', 'Press kit'] },
      { heading: 'Legal', links: ['Privacy', 'Terms', 'DPA', 'Subprocessors'] },
    ]
    
    const sitemapSections = [
      { heading: 'Platform', links: ['Metric layer', 'Dashboards', 'Alerting', 'Embedding', 'API', 'CLI'] },
      { heading: 'Solutions', links: ['Finance', 'RevOps', 'Analytics', 'Engineering', 'Healthcare', 'Retail'] },
      { heading: 'Developers', links: ['Documentation', 'API reference', 'SDKs', 'Webhooks', 'Status', 'Changelog'] },
      { heading: 'Resources', links: ['Guides', 'Benchmarks', 'Customers', 'Blog', 'Webinars', 'Glossary'] },
      { heading: 'Company', links: ['About', 'Careers', 'Press kit', 'Partners', 'Contact', 'Security'] },
      { heading: 'Legal', links: ['Privacy', 'Terms', 'DPA', 'Subprocessors', 'SLA', 'Accessibility'] },
    ]
    
    const denseColumns: { title: string; links: { label: string; href: string; badge?: string }[] }[] = [
      {
        title: 'UI Primitives',
        links: [
          { label: 'Button & Controls', href: '#' },
          { label: 'Dialog & Overlays', href: '#' },
          { label: 'Data Table & Virtual', href: '#' },
          { label: 'Charts & Sparklines', href: '#' },
          { label: 'Form Controls & Inputs', href: '#' },
          { label: 'KpiGrid & Metric Tiles', href: '#' },
        ],
      },
      {
        title: 'Marketing Blocks',
        links: [
          { label: 'Hero Sandboxes', href: '#' },
          { label: 'Feature Workbenches', href: '#' },
          { label: 'Pricing Matrix & ROI', href: '#' },
          { label: 'Social Proof Tickers', href: '#' },
          { label: 'Testimonial Masonry', href: '#' },
          { label: 'Interactive Demos', href: '#', badge: 'New' },
        ],
      },
      {
        title: 'Ecosystem Templates',
        links: [
          { label: 'HRMS Nuxt Template', href: '#' },
          { label: 'HMS Clinical Dashboard', href: '#' },
          { label: 'Shipment Tracking Live', href: '#' },
          { label: 'Fintech Banking Portal', href: '#' },
          { label: 'AI LLM Workspace', href: '#' },
        ],
      },
      {
        title: 'Developer DX',
        links: [
          { label: 'CLI Installation Guide', href: '#' },
          { label: 'Tailwind CSS v4 Tokens', href: '#' },
          { label: 'Dual-Framework Parity', href: '#' },
          { label: 'Reka UI Primitives', href: '#' },
          { label: 'Radix UI Primitives', href: '#' },
          { label: 'Changelog Timeline', href: '#' },
        ],
      },
    ]
    
    const footer01Columns = [
      { title: 'Product', links: ['Features', 'Pricing', 'Changelog', 'Roadmap'] },
      { title: 'Company', links: ['About', 'Blog', 'Careers', 'Contact'] },
      { title: 'Resources', links: ['Documentation', 'API', 'Help center', 'Status'] },
      { title: 'Legal', links: ['Terms', 'Privacy', 'Security', 'DPA'] },
    ]
    
    const minimalLinks = [
      { label: 'Product', href: '#product' },
      { label: 'Pricing', href: '#pricing' },
      { label: 'Docs', href: '#docs' },
      { label: 'Privacy', href: '#privacy' },
      { label: 'Terms', href: '#terms' },
    ]
    
    export type FooterVariant =
      | 'footer-01'
      | 'brand-statement'
      | 'developer-dense-grid'
      | 'mega-newsletter'
      | 'minimal-row'
      | 'sitemap-dense'
    
    /**
     * <uip-footer> — the registry footer block collection as one web component.
     *
     * Class strings are React's Footer01 / FooterBrandStatement /
     * FooterDeveloperDenseGrid / FooterMegaNewsletter / FooterMinimalRow /
     * FooterSitemapDense verbatim; `variant` selects which one renders
     * (`footer-01` by default). Newsletter forms are internal state like React
     * (no callbacks); the sitemap region/currency selects are <uip-select>
     * reading light-DOM <option> children. Brand glyphs stay inline SVGs, the
     * way the React blocks ship them.
     */
    export class UipFooter 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 = {
        variant: { reflect: true },
        newsletter: { state: true },
        subscribed: { state: true },
        megaEmail: { state: true },
        megaSubmitted: { state: true },
        region: { state: true },
        currency: { state: true },
      }
    
      variant: FooterVariant = 'footer-01'
      private newsletter = ''
      private subscribed = false
      private megaEmail = ''
      private megaSubmitted = false
      private region = 'eu'
      private currency = 'eur'
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'footer')
      }
    
      private subscribe01(e: Event) {
        e.preventDefault()
        if (!this.newsletter) return
        this.subscribed = true
        this.newsletter = ''
      }
    
      private subscribeMega(e: Event) {
        e.preventDefault()
        if (!this.megaEmail.trim()) return
        this.megaSubmitted = true
      }
    
      private renderFooter01() {
        return html`
          <footer data-slot="footer-01" class="bg-background border-t">
            <div class="mx-auto max-w-6xl px-6 py-16">
              <div class="grid gap-10 lg:grid-cols-12">
                <div class="space-y-4 lg:col-span-4">
                  <div class="flex items-center gap-2">
                    <div class="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
                      ${icon(Boxes, 'boxes', 'size-4')}
                    </div>
                    <span class="text-base font-semibold">Acme</span>
                  </div>
                  <p class="text-muted-foreground max-w-sm text-sm">Built on shadcn-vue. Get product updates monthly.</p>
                  <form class="flex max-w-sm gap-2" @submit=${this.subscribe01}>
                    <uip-input
                      .value=${this.newsletter}
                      @input=${(e: Event) => {
                        this.newsletter = (e.target as UipInput).value
                      }}
                      type="email"
                      placeholder="you@company.com"
                      required
                      class="flex-1"
                    ></uip-input>
                    <uip-button type="submit">Subscribe</uip-button>
                  </form>
                  ${this.subscribed
                    ? html`<p class="text-success text-xs">Thanks — check your inbox to confirm.</p>`
                    : nothing}
                </div>
    
                <div class="grid grid-cols-2 gap-6 sm:grid-cols-4 lg:col-span-8">
                  ${footer01Columns.map(
                    (col) => html`<div class="space-y-3">
                      <h3 class="text-sm font-semibold">${col.title}</h3>
                      <ul class="space-y-2">
                        ${col.links.map(
                          (link) => html`<li>
                            <a href="#" class="text-muted-foreground hover:text-foreground text-sm transition-colors">
                              ${link}
                            </a>
                          </li>`,
                        )}
                      </ul>
                    </div>`,
                  )}
                </div>
              </div>
    
              <div
                data-uipkge=""
                data-slot="separator"
                data-orientation="horizontal"
                role="none"
                class="bg-border shrink-0 h-px w-full my-10"
              ></div>
    
              <div class="flex flex-wrap items-center justify-between gap-4">
                <p class="text-muted-foreground text-xs">© 2026 Acme. All rights reserved.</p>
                <div class="flex items-center gap-3">
                  <a href="#" aria-label="GitHub" class="text-muted-foreground hover:text-foreground transition-colors">
                    ${githubSvg('size-4')}
                  </a>
                  <a href="#" aria-label="Twitter" class="text-muted-foreground hover:text-foreground transition-colors">
                    ${twitterSvg('size-4')}
                  </a>
                  <a href="#" aria-label="LinkedIn" class="text-muted-foreground hover:text-foreground transition-colors">
                    ${linkedinSvg('size-4')}
                  </a>
                </div>
              </div>
            </div>
          </footer>
        `
      }
    
      private renderBrandStatement() {
        return html`
          <footer data-slot="footer-brand-statement" class="border-border bg-background border-t">
            <div class="mx-auto max-w-6xl px-6 py-16">
              <a href="#top" class="flex w-fit items-center gap-2 font-semibold tracking-tight">
                ${icon(Layers, 'layers', 'text-primary size-5')} Northwind
              </a>
    
              <p class="mt-8 max-w-3xl text-2xl leading-tight font-medium tracking-tight text-balance sm:text-3xl">
                Every number your company acts on should have one definition, one owner, and a history you can read.
              </p>
              <p class="text-muted-foreground mt-4 max-w-xl text-sm leading-relaxed">
                That is the whole product. Everything else here is detail.
              </p>
    
              <div
                data-uipkge=""
                data-slot="separator"
                data-orientation="horizontal"
                role="none"
                class="bg-border shrink-0 h-px w-full my-12"
              ></div>
    
              <div class="grid grid-cols-2 gap-8 sm:grid-cols-3">
                ${brandSections.map(
                  (section) => html`<div>
                    <p class="text-muted-foreground/70 text-xs font-medium tracking-wide uppercase">${section.heading}</p>
                    <ul class="mt-3 space-y-2">
                      ${section.links.map(
                        (link) => html`<li>
                          <a href="#" class="text-muted-foreground hover:text-foreground text-sm transition-colors">
                            ${link}
                          </a>
                        </li>`,
                      )}
                    </ul>
                  </div>`,
                )}
              </div>
    
              <div
                data-uipkge=""
                data-slot="separator"
                data-orientation="horizontal"
                role="none"
                class="bg-border shrink-0 h-px w-full my-10"
              ></div>
    
              <div class="flex flex-col gap-4 sm:flex-row sm:items-center">
                <div class="text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-2 text-xs">
                  <span>© 2026 Northwind Data, Inc.</span>
                  <a href="#privacy" class="hover:text-foreground transition-colors">Privacy</a>
                  <a href="#terms" class="hover:text-foreground transition-colors">Terms</a>
                  <a href="mailto:hello@northwind.example" class="hover:text-foreground transition-colors">
                    hello@northwind.example
                  </a>
                </div>
                <div class="flex items-center gap-2 sm:ml-auto">
                  <uip-badge variant="outline">SOC 2 Type II</uip-badge>
                  <uip-badge variant="outline">GDPR</uip-badge>
                  <uip-button variant="ghost" size="icon-sm" class="size-8" aria-label="GitHub">
                    ${githubSvg('size-4')}
                  </uip-button>
                  <uip-button variant="ghost" size="icon-sm" class="size-8" aria-label="LinkedIn">
                    ${linkedinSvg('size-4')}
                  </uip-button>
                </div>
              </div>
            </div>
          </footer>
        `
      }
    
      private renderDeveloperDenseGrid() {
        const year = new Date().getFullYear()
        return html`
          <footer data-slot="footer-developer-dense-grid" class=${cn('border-border bg-card text-foreground border-t')}>
            <div class="mx-auto max-w-7xl space-y-12 px-4 py-12 sm:px-6 sm:py-16 lg:px-8">
              <div class="grid grid-cols-2 gap-8 text-left md:grid-cols-6 lg:grid-cols-12">
                <div class="col-span-2 space-y-4 md:col-span-6 lg:col-span-4">
                  <div class="flex items-center gap-2 font-mono text-base font-bold tracking-tight">
                    <span
                      class="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-lg text-xs font-bold shadow-xs"
                    >
                      U
                    </span>
                    <span>UIPKGE.DEV</span>
                  </div>
    
                  <p class="text-muted-foreground max-w-sm text-xs leading-relaxed">
                    The open-source unbundled component registry where you own 100% of the source code. Calibrated tokens,
                    pure AST distribution, and mathematical dual-framework parity across Vue 3.5 and React 19.
                  </p>
    
                  <div class="flex items-center gap-2 pt-2">
                    <uip-button size="icon-sm" variant="outline" class="size-8 rounded-lg p-0" aria-label="GitHub">
                      ${githubSvg('size-3.5')}
                    </uip-button>
                    <uip-button size="icon-sm" variant="outline" class="size-8 rounded-lg p-0" aria-label="Twitter">
                      ${twitterSvg('size-3.5')}
                    </uip-button>
                    <uip-button size="icon-sm" variant="outline" class="size-8 rounded-lg p-0" aria-label="Website">
                      ${icon(Globe, 'globe', 'size-3.5')}
                    </uip-button>
                  </div>
                </div>
    
                ${denseColumns.map(
                  (col) => html`<div class="col-span-1 space-y-3 md:col-span-3 lg:col-span-2">
                    <h4 class="text-foreground font-mono text-xs font-bold tracking-wider uppercase">${col.title}</h4>
                    <ul class="text-muted-foreground space-y-2 font-mono text-xs">
                      ${col.links.map(
                        (link) => html`<li>
                          <a href=${link.href} class="hover:text-foreground inline-flex items-center gap-1.5 transition-colors">
                            <span>${link.label}</span>
                            ${link.badge
                              ? html`<span class="py-0.2 bg-primary/20 text-primary rounded px-1.5 text-xs font-bold uppercase">
                                  ${link.badge}
                                </span>`
                              : nothing}
                          </a>
                        </li>`,
                      )}
                    </ul>
                  </div>`,
                )}
              </div>
    
              <div
                class="border-border/80 text-muted-foreground flex flex-col items-center justify-between gap-4 border-t pt-8 text-center font-mono text-xs sm:flex-row sm:text-left"
              >
                <div class="flex items-center gap-2.5">
                  <span class="relative flex size-2">
                    <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span>
                    <span class="bg-success relative inline-flex size-2 rounded-full"></span>
                  </span>
                  <span class="text-foreground font-medium">All Global Edge Nodes Operational</span>
                  <span class="text-muted-foreground/60">&bull; Latency: 14ms (P99)</span>
                </div>
    
                <div class="flex items-center gap-3">
                  <span>MIT Licensed &bull; Unbundled Code</span>
                  <span>&copy; ${year} UIPKGE</span>
                </div>
              </div>
            </div>
          </footer>
        `
      }
    
      private renderMegaNewsletter() {
        return html`
          <footer data-slot="footer-mega-newsletter" class="border-border bg-background border-t">
            <div class="mx-auto max-w-6xl px-6 py-14">
              <div class="grid gap-12 lg:grid-cols-[22rem_1fr] lg:gap-16">
                <div>
                  <a href="#top" class="flex items-center gap-2 font-semibold tracking-tight">
                    ${icon(Layers, 'layers', 'text-primary size-5')} Northwind
                  </a>
                  <p class="text-muted-foreground mt-3 max-w-sm text-sm leading-relaxed">
                    One certified metric layer between your warehouse and everything downstream.
                  </p>
    
                  <form class="mt-6" @submit=${this.subscribeMega}>
                    <label for="footer-email" class="text-sm font-medium">Monthly engineering notes</label>
                    <div class="mt-2 flex gap-2">
                      <uip-input
                        id="footer-email"
                        .value=${this.megaEmail}
                        @input=${(e: Event) => {
                          this.megaEmail = (e.target as UipInput).value
                        }}
                        type="email"
                        placeholder="you@company.com"
                        autocomplete="email"
                        class="max-w-64"
                      ></uip-input>
                      <uip-button type="submit">
                        Subscribe ${icon(ArrowRight, 'arrow-right', 'ml-1.5 size-3.5')}
                      </uip-button>
                    </div>
                    <p class="text-muted-foreground mt-2 min-h-5 text-xs">
                      ${this.megaSubmitted
                        ? html`<span class="text-success inline-flex items-center gap-1.5">
                            ${icon(Check, 'check', 'size-3')} Check your inbox to confirm.
                          </span>`
                        : html`<span>One email a month. Unsubscribe in one click.</span>`}
                    </p>
                  </form>
                </div>
    
                <div class="grid grid-cols-2 gap-8 sm:grid-cols-3 lg:grid-cols-5">
                  ${megaSections.map(
                    (section) => html`<div>
                      <p class="text-muted-foreground/70 text-xs font-medium tracking-wide uppercase">${section.heading}</p>
                      <ul class="mt-3 space-y-2">
                        ${section.links.map(
                          (link) => html`<li>
                            <a href="#" class="text-muted-foreground hover:text-foreground text-sm transition-colors">
                              ${link}
                            </a>
                          </li>`,
                        )}
                      </ul>
                    </div>`,
                  )}
                </div>
              </div>
    
              <div
                data-uipkge=""
                data-slot="separator"
                data-orientation="horizontal"
                role="none"
                class="bg-border shrink-0 h-px w-full my-10"
              ></div>
    
              <div class="flex flex-col gap-4 sm:flex-row sm:items-center">
                <p class="text-muted-foreground text-xs">© 2026 Northwind Data, Inc. All rights reserved.</p>
                <uip-badge variant="outline" class="w-fit">SOC 2 Type II</uip-badge>
                <div class="flex items-center gap-1 sm:ml-auto">
                  <uip-button variant="ghost" size="icon-sm" class="size-8" aria-label="GitHub">
                    ${githubSvg('size-4')}
                  </uip-button>
                  <uip-button variant="ghost" size="icon-sm" class="size-8" aria-label="LinkedIn">
                    ${linkedinSvg('size-4')}
                  </uip-button>
                  <uip-button variant="ghost" size="icon-sm" class="size-8" aria-label="YouTube">
                    ${youtubeSvg('size-4')}
                  </uip-button>
                </div>
              </div>
            </div>
          </footer>
        `
      }
    
      private renderMinimalRow() {
        return html`
          <footer data-slot="footer-minimal-row" class="border-border bg-background border-t">
            <div class="mx-auto flex max-w-6xl flex-col gap-4 px-6 py-6 sm:flex-row sm:items-center">
              <a href="#top" class="flex shrink-0 items-center gap-2 text-sm font-semibold tracking-tight">
                ${icon(Layers, 'layers', 'text-primary size-4')} Northwind
              </a>
    
              <div
                data-uipkge=""
                data-slot="separator"
                data-orientation="vertical"
                role="none"
                class="bg-border shrink-0 w-px hidden h-4 sm:block"
              ></div>
    
              <nav class="flex flex-wrap items-center gap-x-5 gap-y-2" aria-label="Footer">
                ${minimalLinks.map(
                  (link) => html`<a
                    href=${link.href}
                    class="text-muted-foreground hover:text-foreground text-sm transition-colors"
                  >
                    ${link.label}
                  </a>`,
                )}
              </nav>
    
              <div class="flex items-center gap-1 sm:ml-auto">
                <uip-button variant="ghost" size="icon-sm" class="size-8" aria-label="GitHub">
                  ${githubSvg('size-4')}
                </uip-button>
                <uip-button variant="ghost" size="icon-sm" class="size-8" aria-label="LinkedIn">
                  ${linkedinSvg('size-4')}
                </uip-button>
                <span class="text-muted-foreground ml-2 text-xs">© 2026 Northwind</span>
              </div>
            </div>
          </footer>
        `
      }
    
      private renderSitemapDense() {
        return html`
          <footer data-slot="footer-sitemap-dense" class="border-border bg-background border-t">
            <div class="mx-auto max-w-6xl px-6 py-12">
              <div class="grid grid-cols-2 gap-x-6 gap-y-8 sm:grid-cols-3 lg:grid-cols-6">
                ${sitemapSections.map(
                  (section) => html`<div>
                    <p class="text-muted-foreground/70 text-xs font-medium tracking-wide uppercase">${section.heading}</p>
                    <ul class="mt-3 space-y-1.5">
                      ${section.links.map(
                        (link) => html`<li>
                          <a href="#" class="text-muted-foreground hover:text-foreground text-sm transition-colors">
                            ${link}
                          </a>
                        </li>`,
                      )}
                    </ul>
                  </div>`,
                )}
              </div>
    
              <div
                data-uipkge=""
                data-slot="separator"
                data-orientation="horizontal"
                role="none"
                class="bg-border shrink-0 h-px w-full my-10"
              ></div>
    
              <div class="flex flex-col gap-6 lg:flex-row lg:items-center">
                <a href="#top" class="flex shrink-0 items-center gap-2 text-sm font-semibold tracking-tight">
                  ${icon(Layers, 'layers', 'text-primary size-4')} Northwind
                </a>
    
                <div class="flex flex-wrap items-center gap-2">
                  <uip-select
                    class="h-8 w-40 text-xs"
                    aria-label="Data region"
                    .value=${this.region}
                    @change=${(e: Event) => {
                      this.region = (e.target as UipSelect).value
                    }}
                  >
                    <option value="eu">EU (Frankfurt)</option>
                    <option value="us">US (Virginia)</option>
                    <option value="au">AU (Sydney)</option>
                  </uip-select>
    
                  <uip-select
                    class="h-8 w-28 text-xs"
                    aria-label="Currency"
                    .value=${this.currency}
                    @change=${(e: Event) => {
                      this.currency = (e.target as UipSelect).value
                    }}
                  >
                    <option value="eur">EUR €</option>
                    <option value="usd">USD $</option>
                    <option value="gbp">GBP £</option>
                  </uip-select>
                </div>
    
                <div class="flex flex-wrap items-center gap-3 lg:ml-auto">
                  <a href="#status" class="hover:bg-muted flex items-center gap-2 rounded-md px-2 py-1 transition-colors">
                    <span class="bg-success size-2 rounded-full" aria-hidden="true"></span>
                    <span class="text-muted-foreground text-xs">All systems operational</span>
                  </a>
                  <uip-badge variant="outline">SOC 2</uip-badge>
                  <uip-badge variant="outline">GDPR</uip-badge>
                  <uip-button variant="ghost" size="icon-sm" class="size-8" aria-label="GitHub">
                    ${githubSvg('size-4')}
                  </uip-button>
                  <uip-button variant="ghost" size="icon-sm" class="size-8" aria-label="LinkedIn">
                    ${linkedinSvg('size-4')}
                  </uip-button>
                </div>
              </div>
    
              <p class="text-muted-foreground mt-6 text-xs">© 2026 Northwind Data, Inc. All rights reserved.</p>
            </div>
          </footer>
        `
      }
    
      render() {
        switch (this.variant) {
          case 'brand-statement':
            return this.renderBrandStatement()
          case 'developer-dense-grid':
            return this.renderDeveloperDenseGrid()
          case 'mega-newsletter':
            return this.renderMegaNewsletter()
          case 'minimal-row':
            return this.renderMinimalRow()
          case 'sitemap-dense':
            return this.renderSitemapDense()
          case 'footer-01':
          default:
            return this.renderFooter01()
        }
      }
    }
    
    customElements.get('uip-footer') || customElements.define('uip-footer', UipFooter)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-footer': UipFooter
      }
    }
    

Raw manifest:https://uipkge.dev/r/lit/footer.json