Menu

Change language

GitHub repo

Hero Split Device Mockup

blockhero

Dual viewport device mockup hero with interactive viewport controls and real-time state synchronization.

Installation

$npx uipkge-ng@latest add hero-split-device-mockup
Manifest:https://uipkge.dev/r/angular/hero-split-device-mockup.jsonInstalls to:src/app/components/blocks/hero-split-device-mockup/

Variants

Loading interactive previews…

Schema

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

HeroSplitKpi
interface HeroSplitKpi {
  label: string
  value: string
  change: string
  status: string
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/hero-split-device-mockup/hero-split-device-mockup.component.ts10.8 kB
    // Dual viewport device mockup hero with interactive viewport controls (desktop /
    // tablet / mobile), dark-frame toggle, inner app tabs, KPI grid and a telemetry
    // sparkline. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import { ArrowRight, LucideAngularModule, Monitor, Moon, Smartphone, Sun, Tablet } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component'
    
    export type HeroSplitDeviceMode = 'desktop' | 'tablet' | 'mobile'
    export type HeroSplitDeviceTab = 'overview' | 'analytics' | 'activity'
    
    interface HeroSplitKpi {
      label: string
      value: string
      change: string
      status: string
    }
    
    const mockKpis: HeroSplitKpi[] = [
      { label: 'Active Sessions', value: '24,892', change: '+14.2%', status: 'up' },
      { label: 'Conversion Rate', value: '4.82%', change: '+0.6%', status: 'up' },
      { label: 'Avg TTFB', value: '18ms', change: '-4ms', status: 'optimal' },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-hero-split-device-mockup, [ui-hero-split-device-mockup]',
      standalone: true,
      // React renders the root <section> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent],
      template: `
        <section data-slot="hero-split-device-mockup" [class]="rootClass">
          <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
            <!-- Top Title & Badge Area -->
            <div class="mx-auto max-w-3xl space-y-4 text-center">
              <a
                href="#device-preview"
                class="group border-border/80 bg-secondary/60 hover:bg-secondary text-foreground inline-flex items-center gap-2 rounded-full border px-3.5 py-1 text-xs font-medium shadow-2xs transition-colors"
              >
                <lucide-icon [img]="Smartphone" class="text-primary size-3.5" />
                <span>Fluid Container Queries &bull; Tailwind v4 Ready</span>
                <lucide-icon
                  [img]="ArrowRight"
                  class="text-muted-foreground size-3 transition-transform group-hover:translate-x-0.5"
                />
              </a>
    
              <h1 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
                {{ title }}
              </h1>
    
              <p class="text-muted-foreground text-base sm:text-lg">
                {{ description }}
              </p>
    
              <!-- Viewport Controls & Interactive Triggers -->
              <div class="flex flex-wrap items-center justify-center gap-2 pt-2">
                <div class="border-border bg-muted/50 inline-flex rounded-lg border p-1 shadow-2xs">
                  <button type="button" [class]="modeClass('desktop')" (click)="activeMode.set('desktop')">
                    <lucide-icon [img]="Monitor" class="size-3.5" />
                    <span>Desktop (1440px)</span>
                  </button>
                  <button type="button" [class]="modeClass('tablet')" (click)="activeMode.set('tablet')">
                    <lucide-icon [img]="Tablet" class="size-3.5" />
                    <span>Tablet (768px)</span>
                  </button>
                  <button type="button" [class]="modeClass('mobile')" (click)="activeMode.set('mobile')">
                    <lucide-icon [img]="Smartphone" class="size-3.5" />
                    <span>Mobile (375px)</span>
                  </button>
                </div>
    
                <button
                  ui-button
                  variant="outline"
                  size="sm"
                  class="border-border gap-1.5 text-xs"
                  (click)="toggleDarkMode()"
                >
                  @if (isDarkMode()) {
                    <lucide-icon [img]="Sun" class="size-3.5" />
                  } @else {
                    <lucide-icon [img]="Moon" class="size-3.5" />
                  }
                  <span>{{ isDarkMode() ? 'Dark Frame' : 'Light Frame' }}</span>
                </button>
              </div>
            </div>
    
            <!-- Split Device Mockup Canvas -->
            <div class="mt-12 flex justify-center">
              <div [class]="frameClass">
                <!-- Browser / Device Chrome Header -->
                <div class="border-border/80 flex items-center justify-between border-b px-2 pt-1 pb-2.5">
                  <div class="flex items-center gap-1.5">
                    <span class="bg-destructive/80 size-2.5 rounded-full"></span>
                    <span class="bg-warning/80 size-2.5 rounded-full"></span>
                    <span class="bg-success/80 size-2.5 rounded-full"></span>
                  </div>
    
                  <!-- URL Bar -->
                  <div
                    class="border-border/60 bg-muted/60 text-muted-foreground mx-auto flex h-6 w-1/2 items-center justify-center truncate rounded-md border px-2 font-mono text-xs"
                  >
                    https://uipkge.dev/demo/dashboard-kpis
                  </div>
    
                  <div class="text-muted-foreground flex items-center gap-1">
                    <span class="bg-success size-2 rounded-full"></span>
                    <span class="font-mono text-xs">LIVE</span>
                  </div>
                </div>
    
                <!-- Inside Interactive Device Application -->
                <div class="bg-background space-y-4 rounded-lg p-4 pt-4">
                  <!-- App Header Inside Frame -->
                  <div class="border-border flex items-center justify-between border-b pb-3">
                    <div class="flex items-center gap-2">
                      <div
                        class="bg-primary text-primary-foreground flex size-7 items-center justify-center rounded-md text-xs font-bold"
                      >
                        UI
                      </div>
                      <div>
                        <div class="text-foreground text-xs font-semibold">Platform Metrics</div>
                        <div class="text-muted-foreground text-xs">Production Cluster (us-east-1)</div>
                      </div>
                    </div>
    
                    <!-- Inner App Tabs -->
                    <div class="border-border bg-muted/40 flex gap-1 rounded-md border p-0.5">
                      <button type="button" [class]="tabClass('overview')" (click)="activeTab.set('overview')">
                        Overview
                      </button>
                      <button type="button" [class]="tabClass('analytics')" (click)="activeTab.set('analytics')">
                        Analytics
                      </button>
                    </div>
                  </div>
    
                  <!-- KPI Metric Grid in Frame -->
                  <div [class]="kpiGridClass">
                    @for (kpi of kpis; track kpi.label) {
                      <div ui-card class="border-border bg-card/60 shadow-2xs">
                        <div ui-card-content class="space-y-1 p-3">
                          <div class="text-muted-foreground text-xs font-medium">{{ kpi.label }}</div>
                          <div class="text-foreground text-lg font-bold tracking-tight">{{ kpi.value }}</div>
                          <div class="text-success flex items-center gap-1 text-xs font-medium">
                            <span>{{ kpi.change }}</span>
                            <span class="text-muted-foreground">&bull; vs last week</span>
                          </div>
                        </div>
                      </div>
                    }
                  </div>
    
                  <!-- Interactive Wave / Chart Preview -->
                  <div class="border-border/80 bg-muted/20 space-y-2 rounded-lg border p-3">
                    <div class="flex items-center justify-between text-xs">
                      <span class="text-foreground font-medium">Real-time Telemetry Stream</span>
                      <span ui-badge variant="outline" class="border-border text-success font-mono text-xs">
                        Synced: 0.2ms
                      </span>
                    </div>
    
                    <!-- SVG Mock Sparkline Chart -->
                    <svg
                      class="stroke-primary fill-primary/10 h-16 w-full"
                      viewBox="0 0 300 60"
                      preserveAspectRatio="none"
                      aria-hidden="true"
                    >
                      <path d="M0 45 Q 25 20, 50 35 T 100 25 T 150 15 T 200 30 T 250 10 T 300 20 L 300 60 L 0 60 Z" />
                      <path
                        d="M0 45 Q 25 20, 50 35 T 100 25 T 150 15 T 200 30 T 250 10 T 300 20"
                        fill="none"
                        stroke-width="2"
                      />
                    </svg>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </section>
      `,
    })
    export class UiHeroSplitDeviceMockupComponent {
      protected readonly ArrowRight = ArrowRight
      protected readonly Monitor = Monitor
      protected readonly Moon = Moon
      protected readonly Smartphone = Smartphone
      protected readonly Sun = Sun
      protected readonly Tablet = Tablet
    
      protected readonly kpis = mockKpis
    
      @Input() title = 'Responsive components with identical fidelity on every device.'
      @Input() description =
        'Every primitive and block is engineered with fluid container queries and zero layout shift across desktop, tablet, and mobile displays.'
      @Input('class') className?: string
    
      readonly activeMode = signal<HeroSplitDeviceMode>('desktop')
      readonly isDarkMode = signal(true)
      readonly activeTab = signal<HeroSplitDeviceTab>('overview')
    
      readonly viewportWidthClass = computed(() => {
        const mode = this.activeMode()
        if (mode === 'desktop') return 'w-full max-w-2xl'
        if (mode === 'tablet') return 'w-full max-w-md'
        return 'w-full max-w-[320px]'
      })
    
      get rootClass(): string {
        return cn('bg-background relative overflow-hidden py-16 sm:py-24 lg:py-28', this.className)
      }
    
      get frameClass(): string {
        return cn(
          'border-border bg-card rounded-xl border p-2 shadow-md transition-colors duration-300 ease-out sm:p-3',
          this.viewportWidthClass(),
          this.isDarkMode() ? 'dark' : '',
        )
      }
    
      get kpiGridClass(): string {
        return cn('grid gap-2.5', this.activeMode() === 'mobile' ? 'grid-cols-1' : 'grid-cols-3')
      }
    
      modeClass(mode: HeroSplitDeviceMode): string {
        return cn(
          'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none',
          this.activeMode() === mode
            ? 'bg-background text-foreground shadow-2xs'
            : 'text-muted-foreground hover:text-foreground',
        )
      }
    
      tabClass(tab: HeroSplitDeviceTab): string {
        return cn(
          'rounded px-2 py-0.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none',
          this.activeTab() === tab
            ? 'bg-background text-foreground shadow-2xs'
            : 'text-muted-foreground hover:text-foreground',
        )
      }
    
      toggleDarkMode(): void {
        this.isDarkMode.update((v) => !v)
      }
    }
    
  • src/app/components/blocks/hero-split-device-mockup/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/hero-split-device-mockup.json