Hero Split Device Mockup
blockheroDual viewport device mockup hero with interactive viewport controls and real-time state synchronization.
Installation
$pnpm dlx uipkge-ng@latest add hero-split-device-mockup$npx uipkge-ng@latest add hero-split-device-mockup$yarn dlx uipkge-ng@latest add hero-split-device-mockup$bunx uipkge-ng@latest add hero-split-device-mockupManifest:
https://uipkge.dev/r/angular/hero-split-device-mockup.jsonInstalls to:src/app/components/blocks/hero-split-device-mockup/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.
HeroSplitKpiinterface 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 • 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">• 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
export { UiHeroSplitDeviceMockupComponent, type HeroSplitDeviceMode, type HeroSplitDeviceTab, } from './hero-split-device-mockup.component'
Raw manifest:https://uipkge.dev/r/angular/hero-split-device-mockup.json