Menu

Change language

GitHub repo

Hero Bento Preview Grid

blockmarketing

Asymmetric 3-column bento hero section with interactive OKLCH palette customizer, dual-framework parity toggle, bundle bloat calculator, and edge latency telemetry.

Installation

$npx uipkge-ng@latest add hero-bento-preview-grid
Manifest:https://uipkge.dev/r/angular/hero-bento-preview-grid.jsonInstalls to:src/app/components/blocks/hero-bento-preview-grid/

Variants

Loading interactive previews…

Schema

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

ColorTheme
interface 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 &amp; 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 &amp; 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">&gt;</span>
                    <span> &lt;Button variant="primary"&gt;</span>
                    <div class="text-success mt-0.5 font-medium">&check; 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 &amp; 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">&lt; 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

Raw manifest:https://uipkge.dev/r/angular/hero-bento-preview-grid.json