Menu

Change language

GitHub repo

Bento — Mixed Metrics

blockmarketing

Five-tile bento mixing headline numbers, an inline sparkline, a short list, and a status tile, for the summary band beneath a hero.

Installation

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

Variants

Loading interactive previews…

Files installed (2)

  • src/app/components/blocks/bento-metrics-mixed/bento-metrics-mixed.component.ts5.6 kB
    // Six tiles mixing headline numbers, a sparkline row, a short list, and a status tile —
    // the summary band that sits beneath a hero. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
    import { ArrowUpRight, CircleCheck, LucideAngularModule } 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'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    const series = [4, 9, 7, 14, 11, 18, 16, 23, 21, 28, 26, 33]
    
    const definitions = [
      { name: 'revenue_net', owner: 'finance' },
      { name: 'margin_by_channel', owner: 'finance' },
      { name: 'pipeline_weighted', owner: 'revops' },
    ]
    
    // Inline SVG polyline: a sparkline does not justify a charting dependency.
    function points(values: number[]): string {
      const min = Math.min(...values)
      const max = Math.max(...values)
      const span = max - min || 1
      return values
        .map((value, index) => {
          const x = (index / (values.length - 1)) * 100
          const y = 30 - ((value - min) / span) * 26
          return `${x.toFixed(1)},${y.toFixed(1)}`
        })
        .join(' ')
    }
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-bento-metrics-mixed, [ui-bento-metrics-mixed]',
      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,
        UiSeparatorComponent,
      ],
      template: `
        <section data-slot="bento-metrics-mixed" [class]="rootClass">
          <div class="mx-auto max-w-6xl px-6 py-16">
            <span ui-badge variant="secondary">At a glance</span>
    
            <div class="mt-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
              <div ui-card>
                <div ui-card-content class="p-5">
                  <p class="text-muted-foreground text-xs tracking-wide uppercase">Days to close</p>
                  <p class="font-display mt-2 text-3xl font-bold tracking-tight">4.2</p>
                  <p class="text-success mt-1 text-xs">down from 9.4</p>
                </div>
              </div>
    
              <div ui-card class="sm:col-span-2">
                <div ui-card-content class="p-5">
                  <div class="flex items-baseline justify-between gap-3">
                    <p class="text-muted-foreground text-xs tracking-wide uppercase">Certified metrics published</p>
                    <p class="font-mono text-xs">12 months</p>
                  </div>
                  <p class="font-display mt-2 text-3xl font-bold tracking-tight">340</p>
                  <svg
                    class="text-primary/70 mt-2 h-8 w-full"
                    viewBox="0 0 100 32"
                    preserveAspectRatio="none"
                    aria-hidden="true"
                  >
                    <polyline [attr.points]="sparkPoints" fill="none" stroke="currentColor" stroke-width="1.5" />
                  </svg>
                </div>
              </div>
    
              <div ui-card>
                <div ui-card-content class="flex h-full flex-col p-5">
                  <p class="text-muted-foreground text-xs tracking-wide uppercase">Status</p>
                  <div class="mt-3 flex items-center gap-2">
                    <lucide-icon [img]="CircleCheck" class="text-success size-4" aria-hidden="true" />
                    <p class="text-sm font-medium">All systems operational</p>
                  </div>
                  <p class="text-muted-foreground mt-auto pt-3 font-mono text-xs">99.98% · rolling 90 days</p>
                </div>
              </div>
    
              <div ui-card class="sm:col-span-2">
                <div ui-card-content class="p-5">
                  <p class="text-muted-foreground text-xs tracking-wide uppercase">Most-read definitions</p>
                  <ul class="mt-3 space-y-2">
                    @for (definition of definitionRows; track definition.name) {
                      <li class="flex items-center justify-between gap-3">
                        <span class="min-w-0 truncate font-mono text-xs">{{ definition.name }}</span>
                        <span class="text-muted-foreground shrink-0 text-xs">{{ definition.owner }}</span>
                      </li>
                    }
                  </ul>
                </div>
              </div>
    
              <div ui-card class="sm:col-span-2">
                <div ui-card-content class="flex h-full flex-col justify-between gap-4 p-5 sm:flex-row sm:items-center">
                  <div class="min-w-0">
                    <p class="text-sm font-medium">See the whole workspace</p>
                    <p class="text-muted-foreground mt-1 text-xs">Live demo, no signup, real definitions.</p>
                  </div>
                  <button ui-button variant="outline" size="sm" class="shrink-0">
                    Open the demo
                    <lucide-icon [img]="ArrowUpRight" class="ml-1.5 size-3.5" aria-hidden="true" />
                  </button>
                </div>
              </div>
            </div>
    
            <ui-separator class="mt-6" />
            <p class="text-muted-foreground mt-4 text-xs">Figures from the Northwind workspace, refreshed hourly.</p>
          </div>
        </section>
      `,
    })
    export class UiBentoMetricsMixedComponent {
      protected readonly ArrowUpRight = ArrowUpRight
      protected readonly CircleCheck = CircleCheck
    
      protected readonly definitionRows = definitions
      protected readonly sparkPoints = points(series)
    
      @Input('class') className?: string
    
      get rootClass(): string {
        return cn('bg-background', this.className)
      }
    }
    
    export { points as bentoMetricsMixedPoints }
    
  • src/app/components/blocks/bento-metrics-mixed/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/bento-metrics-mixed.json