Feature Deep-Dive — Alternating Rows
blockmarketingClassic zig-zag feature deep-dive: three full-width rows alternating copy and visual, each with an eyebrow, headline, checklist, inline link, and a bordered preview panel built from real primitives.
Installation
$pnpm dlx uipkge-ng@latest add feature-alternating-rows$npx uipkge-ng@latest add feature-alternating-rows$yarn dlx uipkge-ng@latest add feature-alternating-rows$bunx uipkge-ng@latest add feature-alternating-rowsManifest:
https://uipkge.dev/r/angular/feature-alternating-rows.jsonInstalls to:src/app/components/blocks/feature-alternating-rows/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.
AlternatingRowPreviewLineinterface AlternatingRowPreviewLine {
text: string
tone: 'add' | 'remove' | 'same'
}AlternatingRowPreviewRuleinterface AlternatingRowPreviewRule {
scope: string
value: string
allowed: boolean
}AlternatingRowPreviewStatinterface AlternatingRowPreviewStat {
name: string
detail: string
}FeatureRowinterface FeatureRow {
eyebrow: string
icon: LucideIconData
title: string
body: string
points: string[]
link: string
reverse: boolean
preview: {
label: string
lines?: AlternatingRowPreviewLine[]
rules?: AlternatingRowPreviewRule[]
stats?: AlternatingRowPreviewStat[]
}
}npm dependencies
Files installed (2)
src/app/components/blocks/feature-alternating-rows/feature-alternating-rows.component.ts8.4 kB
// Classic zig-zag feature deep-dive: three full-width rows alternating copy and // visual, each with an eyebrow, headline, checklist, inline link, and a bordered // preview panel built from real primitives. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ArrowRight, Check, GitBranch, ShieldCheck, Zap, LucideAngularModule, type LucideIconData, } 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' // Explicit shape: the three preview panels are structurally different, so the // optional keys keep the union from collapsing when the template narrows. export interface AlternatingRowPreviewLine { text: string tone: 'add' | 'remove' | 'same' } export interface AlternatingRowPreviewRule { scope: string value: string allowed: boolean } export interface AlternatingRowPreviewStat { name: string detail: string } export interface FeatureRow { eyebrow: string icon: LucideIconData title: string body: string points: string[] link: string reverse: boolean preview: { label: string lines?: AlternatingRowPreviewLine[] rules?: AlternatingRowPreviewRule[] stats?: AlternatingRowPreviewStat[] } } const rows: FeatureRow[] = [ { eyebrow: 'Versioning', icon: GitBranch, title: 'Every change is a reviewable diff', body: 'Dashboards, metrics, and permissions are stored as plain files. Changes open a pull request instead of silently overwriting production.', points: ['Branch per change', 'Review before publish', 'One-click revert'], link: 'How version control works', reverse: false, preview: { label: 'metrics/revenue.yml', lines: [ { text: '+ window: trailing_28d', tone: 'add' }, { text: '- window: trailing_30d', tone: 'remove' }, { text: ' owner: finance-analytics', tone: 'same' }, ], }, }, { eyebrow: 'Access', icon: ShieldCheck, title: 'Permissions that follow the row, not the dashboard', body: 'Scope is evaluated at query time from your identity provider, so a shared link never leaks a region or account the viewer cannot see.', points: ['Row-level filters', 'SCIM group sync', 'Audited every query'], link: 'Read the access model', reverse: true, preview: { label: 'Effective access — EMEA analyst', rules: [ { scope: 'region', value: 'EMEA only', allowed: true }, { scope: 'revenue.net', value: 'Visible', allowed: true }, { scope: 'payroll.*', value: 'Blocked', allowed: false }, ], }, }, { eyebrow: 'Performance', icon: Zap, title: 'Cached where it matters, fresh where it counts', body: 'Hot aggregates are materialised on a schedule you control. Everything else falls through to the warehouse so the number is never stale by accident.', points: ['Per-metric freshness', 'Warehouse pass-through', 'Cost ceiling per team'], link: 'See the caching rules', reverse: false, preview: { label: 'Freshness', stats: [ { name: 'Revenue rollup', detail: 'materialised · 5 min' }, { name: 'Pipeline by stage', detail: 'materialised · 1 hr' }, { name: 'Raw event search', detail: 'live query' }, ], }, }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-feature-alternating-rows, [ui-feature-alternating-rows]', 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="feature-alternating-rows" [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"> Built like infrastructure, not a widget </h2> <p class="text-muted-foreground mt-3 text-lg"> Three decisions that stop a reporting layer from rotting after the first quarter. </p> </div> <div class="mt-16 space-y-16 lg:space-y-24"> @for (row of rows; track row.title) { <div class="grid items-center gap-10 lg:grid-cols-2 lg:gap-16"> <div [class]="row.reverse ? 'lg:order-2' : ''"> <div class="text-muted-foreground flex items-center gap-2"> <lucide-icon [img]="row.icon" class="size-4" aria-hidden="true" /> <span class="font-mono text-xs tracking-[0.14em] uppercase">{{ row.eyebrow }}</span> </div> <h3 class="mt-4 text-2xl font-semibold tracking-tight">{{ row.title }}</h3> <p class="text-muted-foreground mt-3 leading-relaxed">{{ row.body }}</p> <ul class="mt-6 space-y-2.5"> @for (point of row.points; track point) { <li class="flex items-start gap-2.5 text-sm"> <lucide-icon [img]="Check" class="text-primary mt-0.5 size-4 shrink-0" aria-hidden="true" /> <span>{{ point }}</span> </li> } </ul> <button ui-button variant="link" class="mt-5 h-auto p-0"> {{ row.link }} <lucide-icon [img]="ArrowRight" class="ml-1.5 size-3.5" aria-hidden="true" /> </button> </div> <div ui-card [class]="row.reverse ? 'lg:order-1' : ''"> <div ui-card-content class="p-0"> <div class="border-border text-muted-foreground border-b px-4 py-2.5 font-mono text-xs"> {{ row.preview.label }} </div> <!-- Diff preview --> @if (row.preview.lines) { <div class="space-y-1 p-4 font-mono text-xs"> @for (line of row.preview.lines; track line.text) { <p [class]="lineClass(line.tone)">{{ line.text }}</p> } </div> } @else if (row.preview.rules) { <!-- Access preview --> <div class="divide-border divide-y"> @for (rule of row.preview.rules; track rule.scope) { <div class="flex items-center justify-between gap-4 px-4 py-3"> <span class="font-mono text-xs">{{ rule.scope }}</span> <span ui-badge [variant]="rule.allowed ? 'secondary' : 'outline'">{{ rule.value }}</span> </div> } </div> } @else { <!-- Freshness preview --> <div class="p-4"> <ul class="space-y-3"> @for (stat of row.preview.stats; track stat.name) { <li> <div class="flex items-baseline justify-between gap-4"> <span class="text-sm font-medium">{{ stat.name }}</span> <span class="text-muted-foreground font-mono text-xs">{{ stat.detail }}</span> </div> <ui-separator class="mt-3" /> </li> } </ul> </div> } </div> </div> </div> } </div> </div> </section> `, }) export class UiFeatureAlternatingRowsComponent { protected readonly ArrowRight = ArrowRight protected readonly Check = Check protected readonly rows = rows @Input('class') className?: string lineClass(tone: 'add' | 'remove' | 'same'): string { return tone === 'add' ? 'text-success' : tone === 'remove' ? 'text-destructive' : 'text-muted-foreground' } get rootClass(): string { return cn('bg-background', this.className) } }src/app/components/blocks/feature-alternating-rows/index.ts0.2 kB
export { UiFeatureAlternatingRowsComponent, type FeatureRow, type AlternatingRowPreviewLine, type AlternatingRowPreviewRule, type AlternatingRowPreviewStat, } from './feature-alternating-rows.component'
Raw manifest:https://uipkge.dev/r/angular/feature-alternating-rows.json