Hero Bento Preview Grid
blockmarketingAsymmetric 3-column bento hero section with interactive OKLCH palette customizer, dual-framework parity toggle, bundle bloat calculator, and edge latency telemetry.
Installation
$pnpm dlx uipkge-ng@latest add hero-bento-preview-grid$npx uipkge-ng@latest add hero-bento-preview-grid$yarn dlx uipkge-ng@latest add hero-bento-preview-grid$bunx uipkge-ng@latest add hero-bento-preview-gridManifest:
https://uipkge.dev/r/angular/hero-bento-preview-grid.jsonInstalls to:src/app/components/blocks/hero-bento-preview-grid/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.
ColorThemeinterface ColorTheme {
name: string
hue: string
hex: string
}npm dependencies
Files installed (2)
src/app/components/blocks/hero-bento-preview-grid/hero-bento-preview-grid.component.ts14.5 kB
// Asymmetric 3-column bento hero with an interactive OKLCH palette customizer, // a dual-framework parity toggle, a bundle bloat calculator, and edge latency // telemetry. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core' import { Activity, ArrowRight, CheckCircle2, Code2, Cpu, Layers, LucideAngularModule, Moon, Palette, Play, Sun, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent } from '@/ui/card/card.component' interface ColorTheme { name: string hue: string hex: string } const colorThemes: ColorTheme[] = [ { name: 'Emerald', hue: 'oklch(0.65 0.22 145)', hex: '#10b981' }, { name: 'Indigo', hue: 'oklch(0.60 0.24 275)', hex: '#6366f1' }, { name: 'Amber', hue: 'oklch(0.75 0.18 70)', hex: '#f59e0b' }, { name: 'Rose', hue: 'oklch(0.65 0.24 15)', hex: '#f43f5e' }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-hero-bento-preview-grid, [ui-hero-bento-preview-grid]', 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], template: ` <section data-slot="hero-bento-preview-grid" [class]="rootClass"> <!-- Center Ambient Glow --> <div class="bg-primary/10 pointer-events-none absolute top-1/4 left-1/2 -z-10 h-96 w-full max-w-5xl -translate-x-1/2 -translate-y-1/2 rounded-full blur-xl" ></div> <div class="mx-auto max-w-7xl space-y-12 text-center"> <!-- Headline & Subtitle --> <div class="mx-auto max-w-3xl space-y-5"> <span ui-badge variant="secondary" class="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs"> <lucide-icon [img]="Layers" class="text-primary size-3.5" /> The Unbundled Design Engineering Platform </span> <h1 class="text-foreground text-4xl leading-[1.12] font-bold tracking-tight sm:text-5xl lg:text-6xl"> Everything design engineers need to build at lightspeed. </h1> <p class="text-muted-foreground text-base leading-relaxed sm:text-lg"> Tactile workbenches, OKLCH fluid color palettes, and guaranteed cross-framework parity—composed right in your hero. </p> <!-- CTA Buttons --> <div class="flex flex-wrap items-center justify-center gap-3 pt-2"> <a ui-button size="lg" href="#explore" class="gap-2 font-semibold shadow-xs"> <span>Explore 300+ Workbenches</span> <lucide-icon [img]="ArrowRight" class="size-4" /> </a> <a ui-button variant="outline" size="lg" href="#docs" class="gap-2 font-medium"> <lucide-icon [img]="Code2" class="text-muted-foreground size-4" /> <span>View Architecture</span> </a> </div> </div> <!-- 3-Column Asymmetric Bento Hero Grid --> <div class="mx-auto grid max-w-6xl grid-cols-1 gap-4 pt-4 text-left sm:gap-6 md:grid-cols-3"> <!-- Tile 1: OKLCH Theme Customizer (2 Cols) --> <div ui-card class="border-border bg-card flex flex-col justify-between space-y-6 rounded-2xl p-6 shadow-sm md:col-span-2" > <div class="space-y-2"> <div class="flex items-center justify-between"> <span class="text-primary inline-flex items-center gap-1.5 font-mono text-xs font-semibold"> <lucide-icon [img]="Palette" class="size-3.5" /> OKLCH Dynamic Theme Engine </span> <span ui-badge variant="outline" class="font-mono text-xs">Zero Runtime CSS</span> </div> <h3 class="text-foreground text-lg font-bold">Interactive Palette & Contrast Synthesizer</h3> <p class="text-muted-foreground text-xs"> Select a brand accent and watch UI components dynamically adjust perceptual contrast. </p> </div> <!-- Color Buttons & Live Preview --> <div class="border-border bg-background/60 space-y-4 rounded-xl border p-4"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> @for (c of themes; track c.name; let idx = $index) { <button type="button" [class]="colorDotClass(idx)" [style.backgroundColor]="c.hex" [attr.aria-label]="'Select ' + c.name + ' theme'" (click)="activeColorIndex.set(idx)" ></button> } </div> <div class="text-muted-foreground flex items-center gap-1 font-mono text-xs"> <span>Mode:</span> <button type="button" class="border-border hover:bg-muted rounded border p-1 transition-[colors,transform] duration-150 active:scale-90" [attr.aria-label]="isDarkPreview() ? 'Switch to light preview' : 'Switch to dark preview'" (click)="isDarkPreview.set(!isDarkPreview())" > @if (!isDarkPreview()) { <lucide-icon [img]="Sun" class="text-warning size-3.5" /> } @else { <lucide-icon [img]="Moon" class="text-muted-foreground size-3.5" /> } </button> </div> </div> <!-- Live Button Demo with Active Hue --> <div class="flex flex-wrap items-center gap-3 pt-2"> <button type="button" class="rounded-lg px-4 py-2 text-xs font-semibold text-white shadow-xs transition-transform active:scale-95" [style.backgroundColor]="themes[activeColorIndex()].hex" > Synthesized Action Button </button> <button type="button" class="border-border bg-background text-foreground hover:bg-muted rounded-lg border px-4 py-2 text-xs font-medium transition-[colors,transform] duration-150 active:scale-95" > Secondary Token </button> </div> </div> </div> <!-- Tile 2: Dual Framework Parity (1 Col) --> <div ui-card class="border-border bg-card flex flex-col justify-between space-y-4 rounded-2xl p-6 shadow-sm"> <div class="space-y-2"> <div class="flex items-center justify-between"> <span class="text-success inline-flex items-center gap-1.5 font-mono text-xs font-semibold"> <lucide-icon [img]="CheckCircle2" class="size-3.5" /> 100% Mirror Parity </span> </div> <h3 class="text-foreground text-lg font-bold">Vue 3.5 & React 19</h3> <p class="text-muted-foreground text-xs"> Identical CVA variant matrix and AST output across both frameworks. </p> </div> <!-- Framework Toggle --> <div class="space-y-3"> <div class="bg-muted/40 border-border grid grid-cols-2 gap-1 rounded-lg border p-1"> <button type="button" [class]="frameworkTabClass('vue')" (click)="activeFramework.set('vue')"> Vue 3.5 SFC </button> <button type="button" [class]="frameworkTabClass('react')" (click)="activeFramework.set('react')"> React 19 TSX </button> </div> <!-- Code snippet snippet --> <div class="bg-muted/30 border-border/80 text-muted-foreground rounded-lg border p-3 font-mono text-xs"> <span class="text-primary font-bold">></span> <span> <Button variant="primary"></span> <div class="text-success mt-0.5 font-medium">✓ CVA Type Contract Validated</div> </div> </div> </div> <!-- Tile 3: Zero Bundle Bloat Calculator (1 Col) --> <div ui-card class="border-border bg-card flex flex-col justify-between space-y-4 rounded-2xl p-6 shadow-sm"> <div class="space-y-2"> <span class="text-warning inline-flex items-center gap-1.5 font-mono text-xs font-semibold"> <lucide-icon [img]="Cpu" class="size-3.5" /> Zero NPM Overhead </span> <h3 class="text-foreground text-lg font-bold">Own Your Source Code</h3> <p class="text-muted-foreground text-xs"> Slide to calculate bundle weight saved compared to monolithic packages. </p> </div> <!-- Slider Widget --> <div class="border-border bg-background/60 space-y-3 rounded-xl border p-4"> <div class="flex items-center justify-between font-mono text-xs"> <span class="text-muted-foreground">Installed Blocks:</span> <span class="text-foreground font-bold">{{ componentCount() }}</span> </div> <input [value]="componentCount()" (input)="setComponentCount($event)" type="range" min="1" max="50" aria-label="Installed blocks" class="accent-primary w-full cursor-pointer" /> <div class="border-border/60 flex items-center justify-between border-t pt-1 font-mono text-xs"> <span class="text-muted-foreground">Bundle Saved:</span> <span class="text-success font-mono font-bold">~{{ estimatedSavings() }} kB</span> </div> </div> </div> <!-- Tile 4: Sub-millisecond Execution Speed (2 Cols) --> <div ui-card class="border-border bg-card flex flex-col justify-between space-y-4 rounded-2xl p-6 shadow-sm md:col-span-2" > <div class="flex items-center justify-between"> <div class="space-y-1"> <span class="text-primary inline-flex items-center gap-1.5 font-mono text-xs font-semibold"> <lucide-icon [img]="Zap" class="size-3.5" /> Instant Hydration & Zero Latency </span> <h3 class="text-foreground text-lg font-bold">Edge Optimized Architecture</h3> </div> <button ui-button size="sm" variant="outline" type="button" class="gap-1.5 font-mono text-xs active:scale-95" (click)="triggerQuery()" > @if (!isRunningQuery()) { <lucide-icon [img]="Play" class="size-3 fill-current" /> } @else { <lucide-icon [img]="Activity" class="size-3 animate-spin" /> } <span>Ping Edge</span> </button> </div> <!-- Latency Telemetry Row --> <div class="grid grid-cols-3 gap-3"> <div class="border-border bg-background/50 space-y-0.5 rounded-lg border p-3"> <p class="text-muted-foreground font-mono text-xs">Round-Trip Latency</p> <p class="text-success font-mono text-xl font-bold">{{ queryLatency() }}</p> </div> <div class="border-border bg-background/50 space-y-0.5 rounded-lg border p-3"> <p class="text-muted-foreground font-mono text-xs">DOM Mutation Time</p> <p class="text-foreground font-mono text-xl font-bold">< 1.2ms</p> </div> <div class="border-border bg-background/50 space-y-0.5 rounded-lg border p-3"> <p class="text-muted-foreground font-mono text-xs">Accessibility Score</p> <p class="text-primary font-mono text-xl font-bold">100 / 100</p> </div> </div> </div> </div> </div> </section> `, }) export class UiHeroBentoPreviewGridComponent { protected readonly Activity = Activity protected readonly ArrowRight = ArrowRight protected readonly CheckCircle2 = CheckCircle2 protected readonly Code2 = Code2 protected readonly Cpu = Cpu protected readonly Layers = Layers protected readonly Moon = Moon protected readonly Palette = Palette protected readonly Play = Play protected readonly Sun = Sun protected readonly Zap = Zap protected readonly themes = colorThemes @Input('class') className?: string readonly activeColorIndex = signal(0) readonly isDarkPreview = signal(true) readonly activeFramework = signal<'vue' | 'react'>('vue') readonly componentCount = signal(12) readonly isRunningQuery = signal(false) readonly queryLatency = signal('12ms') private queryTimer: ReturnType<typeof setTimeout> | undefined constructor() { inject(DestroyRef).onDestroy(() => clearTimeout(this.queryTimer)) } readonly estimatedSavings = computed(() => (this.componentCount() * 42).toFixed(0)) get rootClass(): string { return cn('bg-background relative overflow-hidden px-4 py-16 sm:px-6 sm:py-24 lg:px-8', this.className) } colorDotClass(idx: number): string { return cn( 'size-6 rounded-full border-2 transition-transform active:scale-90', this.activeColorIndex() === idx ? 'border-foreground scale-125 shadow-xs' : 'border-transparent opacity-70 hover:opacity-100', ) } frameworkTabClass(framework: 'vue' | 'react'): string { const active = framework === 'vue' ? 'text-success' : 'text-info' return cn( 'rounded-md py-1.5 font-mono text-xs font-semibold transition-[color,background-color,transform] duration-150 active:scale-95', this.activeFramework() === framework ? `bg-background shadow-xs ${active}` : 'text-muted-foreground', ) } setComponentCount(event: Event): void { const value = Number((event.target as HTMLInputElement).value) if (!Number.isNaN(value)) this.componentCount.set(value) } triggerQuery(): void { this.isRunningQuery.set(true) clearTimeout(this.queryTimer) this.queryTimer = setTimeout(() => { this.queryLatency.set(`${Math.floor(Math.random() * 8) + 8}ms`) this.isRunningQuery.set(false) }, 400) } }src/app/components/blocks/hero-bento-preview-grid/index.ts0.1 kB
export { UiHeroBentoPreviewGridComponent } from './hero-bento-preview-grid.component'
Raw manifest:https://uipkge.dev/r/angular/hero-bento-preview-grid.json