Features — 2×2 Spotlight
blockmarketingFour large feature panels in a two-by-two grid, each with a generous illustration slot built from primitives, a headline, supporting copy, and a quiet inline link.
Installation
$pnpm dlx uipkge-ng@latest add features-2x2-spotlight$npx uipkge-ng@latest add features-2x2-spotlight$yarn dlx uipkge-ng@latest add features-2x2-spotlight$bunx uipkge-ng@latest add features-2x2-spotlightManifest:
https://uipkge.dev/r/angular/features-2x2-spotlight.jsonInstalls to:src/app/components/blocks/features-2x2-spotlight/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.
SpotlightPanelinterface SpotlightPanel {
eyebrow: string
title: string
body: string
link: string
kind: 'lineage' | 'access' | 'budget' | 'release'
}Files installed (2)
src/app/components/blocks/features-2x2-spotlight/features-2x2-spotlight.component.ts7.9 kB
// Four large feature panels in a two-by-two grid, each with a generous illustration // slot built from primitives, a headline, supporting copy, and a quiet inline link. // Port of the React block 1:1 — same four panels, same lineage/access/budget/release // illustration slots, same copy. import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ArrowRight, 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 { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' export interface SpotlightPanel { eyebrow: string title: string body: string link: string kind: 'lineage' | 'access' | 'budget' | 'release' } const defaultPanels: SpotlightPanel[] = [ { eyebrow: 'Modelling', title: 'One definition, every surface', body: 'Dashboards, exports, and the API all resolve the same certified metric, so the number cannot disagree with itself.', link: 'See the metric layer', kind: 'lineage', }, { eyebrow: 'Governance', title: 'Permissions you can prove', body: 'Scope resolves per query from your identity provider, and every evaluation lands in an exportable audit trail.', link: 'Read the access model', kind: 'access', }, { eyebrow: 'Performance', title: 'Budgeted, not throttled', body: 'Each team gets a query budget enforced at run time, with materialised aggregates carrying the frequent load.', link: 'See the caching rules', kind: 'budget', }, { eyebrow: 'Delivery', title: 'Ship without a redeploy', body: 'Publishing a definition change rolls out to every consumer immediately, and reverting is the same one click.', link: 'How releases work', kind: 'release', }, ] const lineage = ['warehouse.orders', 'metrics.revenue_net', 'dashboard.exec_summary'] const access = [ { label: 'EMEA analyst', value: 'Region-scoped' }, { label: 'Finance lead', value: 'Full ledger' }, { label: 'Contractor', value: 'No payroll' }, ] const budgets = [ { team: 'Finance', used: 62 }, { team: 'RevOps', used: 38 }, { team: 'Product', used: 81 }, ] const releases = [ { version: 'v128', note: 'revenue_net → trailing 28d' }, { version: 'v127', note: 'added margin_by_channel' }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-features-2x2-spotlight, [ui-features-2x2-spotlight]', 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, UiProgressComponent, UiSeparatorComponent, ], template: ` <section data-slot="features-2x2-spotlight" [class]="rootClass"> <div class="mx-auto max-w-6xl px-6 py-20 lg:py-28"> <div class="max-w-2xl"> <span ui-badge variant="secondary">Platform</span> <h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Four things it does properly</h2> <p class="text-muted-foreground mt-3 text-lg"> Each panel is a mechanism, not a promise — the illustration is the real shape of the feature. </p> </div> <div class="mt-12 grid gap-5 md:grid-cols-2"> @for (panel of panels; track panel.title) { <div ui-card class="group overflow-hidden"> <div ui-card-content class="flex h-full flex-col p-0"> <!-- Illustration slot: built from primitives so it themes with the page instead of shipping a baked screenshot. --> <div class="bg-muted/40 border-border border-b p-6"> @if (panel.kind === 'lineage') { <div class="space-y-2"> @for (node of lineage; track node; let i = $index) { <div class="border-border bg-card rounded-md border px-3 py-2 font-mono text-xs" [class.ml-8]="i === lineage.length - 1" [class.ml-4]="i === 1" > {{ node }} </div> } </div> } @else if (panel.kind === 'access') { <div class="divide-border border-border bg-card divide-y rounded-md border"> @for (row of access; track row.label) { <div class="flex items-center justify-between gap-3 px-3 py-2"> <span class="text-xs font-medium">{{ row.label }}</span> <span class="text-muted-foreground font-mono text-xs">{{ row.value }}</span> </div> } </div> } @else if (panel.kind === 'budget') { <div class="space-y-3"> @for (row of budgets; track row.team) { <div> <div class="flex items-baseline justify-between gap-3"> <span class="text-xs font-medium">{{ row.team }}</span> <span class="text-muted-foreground font-mono text-xs">{{ row.used }}%</span> </div> <div ui-progress [value]="row.used" [aria-label]="row.team + ' budget used'" class="mt-1.5 h-1.5" ></div> </div> } </div> } @else { <div class="space-y-3"> @for (release of releases; track release.version) { <div class="flex items-baseline gap-3"> <span class="text-primary font-mono text-xs">{{ release.version }}</span> <span class="text-muted-foreground min-w-0 truncate text-xs">{{ release.note }}</span> </div> } <ui-separator /> <p class="text-muted-foreground text-xs">Rolled out to 41 consumers, no redeploy.</p> </div> } </div> <div class="flex flex-1 flex-col p-6"> <p class="text-muted-foreground font-mono text-xs tracking-[0.14em] uppercase"> {{ panel.eyebrow }} </p> <h3 class="mt-2 text-lg font-semibold tracking-tight">{{ panel.title }}</h3> <p class="text-muted-foreground mt-2 text-sm leading-relaxed">{{ panel.body }}</p> <button ui-button variant="link" class="mt-auto h-auto justify-start p-0 pt-4 text-sm"> {{ panel.link }} <lucide-icon [img]="ArrowRight" class="ml-1.5 size-3.5 transition-transform group-hover:translate-x-0.5" aria-hidden="true" /> </button> </div> </div> </div> } </div> </div> </section> `, }) export class UiFeatures2x2SpotlightComponent { protected readonly ArrowRight = ArrowRight protected readonly lineage = lineage protected readonly access = access protected readonly budgets = budgets protected readonly releases = releases @Input() panels: SpotlightPanel[] = defaultPanels.map((p) => ({ ...p })) @Input('class') className?: string get rootClass(): string { return cn('bg-background', this.className) } }src/app/components/blocks/features-2x2-spotlight/index.ts0.1 kB
export { UiFeatures2x2SpotlightComponent, type SpotlightPanel } from './features-2x2-spotlight.component'
Raw manifest:https://uipkge.dev/r/angular/features-2x2-spotlight.json