Menu

Change language

GitHub repo

Feature Deep-Dive — Alternating Rows

blockmarketing

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.

Installation

$npx uipkge-ng@latest add feature-alternating-rows
Manifest:https://uipkge.dev/r/angular/feature-alternating-rows.jsonInstalls to:src/app/components/blocks/feature-alternating-rows/

Variants

Loading interactive previews…

Schema

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

AlternatingRowPreviewLine
interface AlternatingRowPreviewLine {
  text: string
  tone: 'add' | 'remove' | 'same'
}
AlternatingRowPreviewRule
interface AlternatingRowPreviewRule {
  scope: string
  value: string
  allowed: boolean
}
AlternatingRowPreviewStat
interface AlternatingRowPreviewStat {
  name: string
  detail: string
}
FeatureRow
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[]
  }
}

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

Raw manifest:https://uipkge.dev/r/angular/feature-alternating-rows.json